소스 검색

Merged upstream into working tree

Lawrence Agbani 6 년 전
부모
커밋
1ced3ec14e

+ 1 - 0
.gitattributes

@@ -7,3 +7,4 @@
 /.travis.yml export-ignore
 /phpunit.xml export-ignore
 /README.md export-ignore
+/body-params.png export-ignore

+ 8 - 39
CHANGELOG.md

@@ -14,49 +14,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Removed
 
 
-## [2.1.5] - 123h September, 2018
-### Fixed
-- Parse JSON responses from `@transformer` tag for DIngo router (https://github.com/mpociot/laravel-apidoc-generator/pull/323)
-
-## [2.1.4] - 12th September, 2018
-### Fixed
-- Parse JSON responses from  `@response` and `@transformer` tags correctly (https://github.com/mpociot/laravel-apidoc-generator/pull/321)
-
-## [2.1.3] - 11th September, 2018
-### Fixed
-- Parse `@response` tags regardless of HTTP method (https://github.com/mpociot/laravel-apidoc-generator/pull/318)
-
-## [2.1.2] - 10th September, 2018
-### Fixed
-- Set correct HTTP method when parsing FormRequest (https://github.com/mpociot/laravel-apidoc-generator/pull/314)
-
-## [2.1.1] - 10th September, 2018
-### Fixed
-- Print the correct file path of generated documentation (https://github.com/mpociot/laravel-apidoc-generator/pull/311)
-- Removed any extra slashes in URLs displayed in code samples (https://github.com/mpociot/laravel-apidoc-generator/pull/310)
-- Response calls are now also made for only GET in DIngo Router (https://github.com/mpociot/laravel-apidoc-generator/pull/309)
-- HEAD routes are no longer automatically generated for GET routes in DIngo Router (https://github.com/mpociot/laravel-apidoc-generator/pull/309)
-
-## [2.1.0] - 9th September, 2018
+## [3.0] - unreleased
 ### Added
-- Added support for multiple route domains (https://github.com/mpociot/laravel-apidoc-generator/pull/255) 
-- Added support for descriptions in custom validation rules (https://github.com/mpociot/laravel-apidoc-generator/pull/208)
-- Added support for multiple route prefixes (https://github.com/mpociot/laravel-apidoc-generator/pull/203)
-- Added support for formatting and `<aside>` tags (https://github.com/mpociot/laravel-apidoc-generator/pull/261)
-- Support for Laravel 5.5 auto-discovery (https://github.com/mpociot/laravel-apidoc-generator/pull/217)
+- `@bodyParam` annotation (https://github.com/mpociot/laravel-apidoc-generator/pull/362, https://github.com/mpociot/laravel-apidoc-generator/pull/366)
+- `@authenticated` annotation (https://github.com/mpociot/laravel-apidoc-generator/pull/369)
+- Ability to override the controller `@group` from the method. (https://github.com/mpociot/laravel-apidoc-generator/pull/372)
 
 ### Changed
-- Response calls are now only made when route is GET (https://github.com/mpociot/laravel-apidoc-generator/pull/279)
-- Validator factory is now passed to `FormRequest::validator` method (https://github.com/mpociot/laravel-apidoc-generator/pull/236)
-- Bind optional model parameters in routes (https://github.com/mpociot/laravel-apidoc-generator/pull/297/)
-- HEAD routes are no longer automatically generated for GET routes (https://github.com/mpociot/laravel-apidoc-generator/pull/180)
-- `actAsUserId` option is no longer cast to an int (https://github.com/mpociot/laravel-apidoc-generator/pull/257)
+- Moved from command-line options to a config file  (https://github.com/mpociot/laravel-apidoc-generator/pull/362)
+- Commands have been renamed to the `apidoc` namespace (previously `api`). (https://github.com/mpociot/laravel-apidoc-generator/pull/350)
+- The `update` command has been renamed to `rebuild` and now uses the output path configured in the config file. (https://github.com/mpociot/laravel-apidoc-generator/pull/370)
+- `@resource` renamed to `@group` (https://github.com/mpociot/laravel-apidoc-generator/pull/371)
 
 ### Fixed
-- `useMiddleware` option is now actually used (https://github.com/mpociot/laravel-apidoc-generator/pull/297/)
-- Changes to the info vendor view are now persisted (https://github.com/mpociot/laravel-apidoc-generator/pull/120)
-- Fixed memory leak issues (https://github.com/mpociot/laravel-apidoc-generator/pull/256)
-- Fixed issues with validating array parameters (https://github.com/mpociot/laravel-apidoc-generator/pull/299)
-- `@response` tag now parses content correctly as JSON (https://github.com/mpociot/laravel-apidoc-generator/pull/271)
 
 ### Removed

+ 59 - 26
README.md

@@ -1,6 +1,6 @@
 ## Laravel API Documentation Generator
 
-Automatically generate your API documentation from your existing Laravel/[Dingo](https://github.com/dingo/api) routes. [Here's what the output looks like](http://marcelpociot.de/whiteboard/).
+Automatically generate your API documentation from your existing Laravel/Lumen/[Dingo](https://github.com/dingo/api) routes. [Here's what the output looks like](http://marcelpociot.de/whiteboard/).
 
 `php artisan apidoc:generate`
 
@@ -17,12 +17,7 @@ Automatically generate your API documentation from your existing Laravel/[Dingo]
 > Note: version 3.x requires PHP 7 and Laravel 5.5 or higher.
 
 ```sh
-$ composer require mpociot/laravel-apidoc-generator
-```
-Using Laravel < 5.5? Go to your `config/app.php` and add the service provider:
-
-```php
-Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class,
+$ composer require mpociot/laravel-apidoc-generator:dev-master
 ```
 
 Then publish the config file by running:
@@ -70,7 +65,7 @@ return [
 ];
 ```
 
-This means documentation will be generated for routes in all domains ('***' is a wildcard meaning 'any character') which match any of the patterns 'api/*' or 'v2-api/*', excluding the 'users.create' route, and including the 'users.index' route. (The `versions` key is ignored unless you are using Dingo router).
+This means documentation will be generated for routes in all domains ('&ast;' is a wildcard meaning 'any character') which match any of the patterns 'api/&ast;' or 'v2-api/&ast;', excluding the 'users.create' route, and including the 'users.index' route. (The `versions` key is ignored unless you are using Dingo router).
 Also, in the generated documentation, these routes will have the header 'Authorization: Bearer: {token}' added to the example requests.
 
 You can also separate routes into groups to apply different rules to them:
@@ -133,43 +128,61 @@ This package uses these resources to generate the API documentation:
 
 This package uses the HTTP controller doc blocks to create a table of contents and show descriptions for your API methods.
 
-Using `@group` in a doc block prior to each controller is useful as it creates a Group within the API documentation for all methods defined in that controller (rather than listing every method in a single list for all your controllers), but using `@resource` is not required. The short description after the `@resource` should be unique to allow anchor tags to navigate to this section. A longer description can be included below. Custom formatting and `<aside>` tags are also supported. (see the [Documentarian docs](http://marcelpociot.de/documentarian/installation/markdown_syntax))
+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 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 `<aside>` tags are also supported. (see the [Documentarian docs](http://marcelpociot.de/documentarian/installation/markdown_syntax))
+
+ > Note: using `@group` is optional. Ungrouped routes will be placed in a "general" group.
 
 Above each method within the controller you wish to include in your API documentation you should have a doc block. This should include a unique short description as the first entry. An optional second entry can be added with further information. Both descriptions will appear in the API documentation in a different format as shown below.
+You can also specify an `@group` on a single method to override the group defined at the controller level.
 
 ```php
 /**
- * @resource Example
+ * @group User management
  *
- * Longer description
+ * APIs for managing users
  */
-class ExampleController extends Controller {
+class UserController extends Controller
+{
 
 	/**
-	 * This is the short description [and should be unique as anchor tags link to this in navigation menu]
+	 * Create a user
 	 *
-	 * This can be an optional longer description of your API call, used within the documentation.
+	 * [Insert optional longer description of the API endpoint here.]
+	 *
+	 */
+	 public function createUser()
+	 {
+
+	 }
+	 
+	/**
+	 * @group Account management
 	 *
 	 */
-	 public function foo(){
+	 public function changePassword()
+	 {
 
 	 }
+}
 ```
 
 **Result:** 
 
 ![Doc block result](http://headsquaredsoftware.co.uk/images/api_generator_docblock.png)
 
-### Specifying request body parameters
+### Specifying request parameters
 
-To specify a list of valid parameters your API route accepts, use the `@bodyParam` annotation. It takes the name of the parameter, its type, an optional "required" label, and then its description
+To specify a list of valid parameters your API route accepts, use the `@bodyParam` and `@queryParam` annotations.
+- The `@bodyParam` annotation takes the name of the parameter, its type, an optional "required" label, and then its description.
+- The `@queryParam` annotation (coming soon!) takes the name of the parameter, an optional "required" label, and then its description
 
 
 ```php
 /**
  * @bodyParam title string required The title of the post.
  * @bodyParam body string required The title of the post.
- * @bodyParam type The type of post to create. Defaults to 'textophonious'.
+ * @bodyParam type string The type of post to create. Defaults to 'textophonious'.
+ * @bodyParam author_id int the ID of the author
  * @bodyParam thumbnail image This is required if the post type is 'imagelicious'.
  */
 public function createPost()
@@ -180,11 +193,15 @@ public function createPost()
 
 They will be included in the generated documentation text and example requests.
 
-**Result:** ![Form Request](http://marcelpociot.de/documentarian/form_request.png)
+**Result:**
 
-### Providing an example response
-You can provide an example response for a route. This will be disaplyed in the examples section. There are several ways of doing this.
+![](body_params.png)
 
+### Indicating auth status
+You can use the `@authenticated` annotation on a method to indicate if the endpoint is authenticated. A "Requires authentication" badge will be added to that route in the generated documentation.
+
+### Providing an example response
+You can provide an example response for a route. This will be displayed in the examples section. There are several ways of doing this.
 
 #### @response
 You can provide an example response for a route by using the `@response` annotation with valid JSON:
@@ -206,7 +223,7 @@ public function show($id)
 #### @transformer, @transformerCollection, and @transformerModel
 You can define the transformer that is used for the result of the route using the `@transformer` tag (or `@transformerCollection` if the route returns a list). The package will attempt to generate an instance of the model to be transformed using the following steps, stopping at the first successful one:
 
-1. Check if there is a `@transformerModel` tag to define the model being transformed. If there is none, use the class of the first parameter to the method.
+1. Check if there is a `@transformerModel` tag to define the model being transformed. If there is none, use the class of the first parameter to the transformer's `transform()` method.
 2. Get an instance of the model from the Eloquent model factory
 2. If the parameter is an Eloquent model, load the first from the database.
 3. Create an instance using `new`.
@@ -244,11 +261,27 @@ public function showUser(int $id)
 ```
 For the first route above, this package will generate a set of two users then pass it through the transformer. For the last two, it will generate a single user and then pass it through the transformer.
 
-#### Postman collections
+> Note: for transformer support, you need to install the league/fractal package
+
+```bash
+composer require league/fractal
+```
+
+#### Gnerating responses automatically
+If you don't specify an example response using any of the above means, this package will attempt to get a sample response by making a request to the route (a "response call"). A few things to note about response calls:
+- They are done within a database transaction and changes are rolled back afterwards.
+- The configuration for response calls is located in the `config/apidoc.php`. They are configured within the `['apply']['response_calls']` section for each route group, allowing you to apply different settings for different sets of routes.
+- By default, response calls are only made for GET routes, but you can configure this. Set the `methods` key to an array of methods or '*' to mean all methods. Leave it as an empty array to turn off response calls for that route group.
+- Parameters in URLs (example: `/users/{user}`, `/orders/{id?}`) will be replaced with '1' by default. You can configure this, however.Put the parameter names (including curly braces and question marks) as the keys and their replacements as the values in the `bindings` key.
+- You can configure environment variables (this is useful so you can prevent external services like notifications from being triggered). By default the APP_ENV is set to 'documentation'. You can add more variables in the `env` key.
+- You can also configure what headers, query parameters and body parameters should be sent when making the request (the `headers`, `query`, and `body` keys respectively).
+
+
+### Postman collections
 
 The generator automatically creates a Postman collection file, which you can import to use within your [Postman app](https://www.getpostman.com/apps) for even simpler API testing and usage.
 
-If you don't want to create a Postman collection, set the `--postman` config option to false.
+If you don't want to create a Postman collection, set the `postman` config option to false.
 
 The default base URL added to the Postman collection will be that found in your Laravel `config/app.php` file. This will likely be `http://localhost`. If you wish to change this setting you can directly update the url or link this config value to your environment file to make it more flexible (as shown below):
 
@@ -267,10 +300,10 @@ APP_URL=http://yourapp.app
 If you want to modify the content of your generated documentation, go ahead and edit the generated `index.md` file.
 The default location of this file is: `public/docs/source/index.md`.
  
-After editing the markdown file, use the `apidoc:update` command to rebuild your documentation as a static HTML file.
+After editing the markdown file, use the `apidoc:rebuild` command to rebuild your documentation as a static HTML file.
 
 ```sh
-$ php artisan apidoc:update
+$ php artisan apidoc:rebuild
 ```
 
 As an optional parameter, you can use `--location` to tell the update command where your documentation can be found.

BIN
body_params.png


+ 5 - 1
composer.json

@@ -28,7 +28,11 @@
         "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"
+        "mockery/mockery": "^1.2.0",
+        "league/fractal": "^0.17.0"
+    },
+    "suggest": {
+        "league/fractal": "Required for transformers support"
     },
     "autoload": {
         "psr-4": {

+ 56 - 0
config/apidoc.php

@@ -84,6 +84,62 @@ return [
                     // 'Authorization' => 'Bearer: {token}',
                     // 'Api-Version' => 'v2',
                 ],
+
+                /*
+                 * If no @response or @transformer declaratons are found for the route,
+                 * we'll try to get a sample response by attempting an API call.
+                 * Configure the settings for the API call here,
+                 */
+                'response_calls' => [
+                    /*
+                     * API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc).
+                     * List the methods here or use '*' to mean all methods. Leave empty to disable API calls.
+                     */
+                    'methods' => ['GET'],
+
+                    /*
+                     * For URLs which have parameters (/users/{user}, /orders/{id?}),
+                     * specify what values the parameters should be replaced with.
+                     * Note that you must specify the full parameter, including curly brackets and question marks if any.
+                     */
+                    'bindings' => [
+                        // '{user}' => 1
+                    ],
+
+                    /*
+                     * Environment variables which should be set for the API call.
+                     * This is a good place to ensure that notifications, emails
+                     * and other external services are not triggered during the documentation API calls
+                     */
+                    'env' => [
+                        'APP_ENV' => 'documentation',
+                        'APP_DEBUG' => false,
+                        // 'env_var' => 'value',
+                    ],
+
+                    /*
+                     * Headers which should be sent with the API call.
+                     */
+                    'headers' => [
+                        'Content-Type' => 'application/json',
+                        'Accept' => 'application/json',
+                        // 'key' => 'value',
+                    ],
+
+                    /*
+                     * Query parameters which should be sent with the API call.
+                     */
+                    'query' => [
+                        // 'key' => 'value',
+                    ],
+
+                    /*
+                     * Body parameters which should be sent with the API call.
+                     */
+                    'body' => [
+                        // 'key' => 'value',
+                    ],
+                ],
             ],
         ],
     ],

+ 38 - 30
resources/views/partials/route.blade.php

@@ -1,43 +1,51 @@
-<!-- START_{{$parsedRoute['id']}} -->
-@if($parsedRoute['title'] != '')## {{ $parsedRoute['title']}}
-@else## {{$parsedRoute['uri']}}
-@endif
-@if($parsedRoute['description'])
+<!-- START_{{$route['id']}} -->
+@if($route['title'] != '')## {{ $route['title']}}
+@else## {{$route['uri']}}@endif
+@if($route['authenticated'])<small style="
+  padding: 1px 9px 2px;
+  font-weight: bold;
+  white-space: nowrap;
+  color: #ffffff;
+  -webkit-border-radius: 9px;
+  -moz-border-radius: 9px;
+  border-radius: 9px;
+  background-color: #3a87ad;">Requires authentication</small>@endif
+@if($route['description'])
 
-{!! $parsedRoute['description'] !!}
+{!! $route['description'] !!}
 @endif
 
 > Example request:
 
 ```bash
-curl -X {{$parsedRoute['methods'][0]}} {{$parsedRoute['methods'][0] == 'GET' ? '-G ' : ''}}"{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($parsedRoute['uri'], '/') }}" \
-    -H "Accept: application/json"@if(count($parsedRoute['headers'])) \
-@foreach($parsedRoute['headers'] as $header => $value)
-    -H "{{$header}}"="{{$value}}" @if(! ($loop->last))\
+curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($route['uri'], '/') }}" \
+    -H "Accept: application/json"@if(count($route['headers'])) \
+@foreach($route['headers'] as $header => $value)
+    -H "{{$header}}: {{$value}}" @if(! ($loop->last))\
     @endif
 @endforeach
 @endif
-@if(count($parsedRoute['bodyParameters'])) \
-@foreach($parsedRoute['bodyParameters'] as $attribute => $parameter)
+
+@if(count($route['bodyParameters'])) \
+@foreach($route['bodyParameters'] as $attribute => $parameter)
     -d "{{$attribute}}"="{{$parameter['value']}}" @if(! ($loop->last))\
     @endif
 @endforeach
 @endif
-
 ```
 
 ```javascript
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "{{ rtrim(config('app.docs_url') ?: config('app.url'), '/') }}/{{ ltrim($parsedRoute['uri'], '/') }}",
-    "method": "{{$parsedRoute['methods'][0]}}",
-    @if(count($parsedRoute['bodyParameters']))
-"data": {!! str_replace("\n}","\n    }", str_replace('    ','        ',json_encode(array_combine(array_keys($parsedRoute['bodyParameters']), array_map(function($param){ return $param['value']; },$parsedRoute['bodyParameters'])), JSON_PRETTY_PRINT))) !!},
+    "url": "{{ rtrim(config('app.docs_url') ?: config('app.url'), '/') }}/{{ ltrim($route['uri'], '/') }}",
+    "method": "{{$route['methods'][0]}}",
+    @if(count($route['bodyParameters']))
+"data": {!! str_replace("\n}","\n    }", str_replace('    ','        ',json_encode(array_combine(array_keys($route['bodyParameters']), array_map(function($param){ return $param['value']; },$route['bodyParameters'])), JSON_PRETTY_PRINT))) !!},
     @endif
 "headers": {
         "accept": "application/json",
-@foreach($parsedRoute['headers'] as $header => $value)
+@foreach($route['headers'] as $header => $value)
         "{{$header}}": "{{$value}}",
 @endforeach
     }
@@ -48,40 +56,40 @@ $.ajax(settings).done(function (response) {
 });
 ```
 
-@if(in_array('GET',$parsedRoute['methods']) || (isset($parsedRoute['showresponse']) && $parsedRoute['showresponse']))
+@if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse']))
 > Example response:
 
 ```json
-@if(is_object($parsedRoute['response']) || is_array($parsedRoute['response']))
-{!! json_encode($parsedRoute['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
+@if(is_object($route['response']) || is_array($route['response']))
+{!! json_encode($route['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
 @else
-{!! json_encode(json_decode($parsedRoute['response']), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
+{!! json_encode(json_decode($route['response']), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
 @endif
 ```
 @endif
 
 ### HTTP Request
-@foreach($parsedRoute['methods'] as $method)
-`{{$method}} {{$parsedRoute['uri']}}`
+@foreach($route['methods'] as $method)
+`{{$method}} {{$route['uri']}}`
 
 @endforeach
-@if(count($parsedRoute['bodyParameters']))
+@if(count($route['bodyParameters']))
 #### Body Parameters
 
 Parameter | Type | Status | Description
 --------- | ------- | ------- | ------- | -----------
-@foreach($parsedRoute['bodyParameters'] as $attribute => $parameter)
-    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! implode(' ',$parameter['description']) !!}
+@foreach($route['bodyParameters'] as $attribute => $parameter)
+    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
 @endforeach
 @endif
-@if(count($parsedRoute['queryParameters']))
+@if(count($route['queryParameters']))
 #### Query Parameters
 
 Parameter | Status | Description
 --------- | ------- | ------- | -----------
-@foreach($parsedRoute['queryParameters'] as $attribute => $parameter)
+@foreach($route['queryParameters'] as $attribute => $parameter)
     {{$attribute}} | @if($parameter['required']) required @else optional @endif | {!! implode(' ',$parameter['description']) !!}
 @endforeach
 @endif
 
-<!-- END_{{$parsedRoute['id']}} -->
+<!-- END_{{$route['id']}} -->

+ 8 - 11
src/Commands/GenerateDocumentation.php

@@ -8,11 +8,9 @@ use Illuminate\Console\Command;
 use Mpociot\Reflection\DocBlock;
 use Illuminate\Support\Collection;
 use Mpociot\ApiDoc\Tools\RouteMatcher;
+use Mpociot\ApiDoc\Generators\Generator;
 use Mpociot\Documentarian\Documentarian;
 use Mpociot\ApiDoc\Postman\CollectionWriter;
-use Mpociot\ApiDoc\Generators\DingoGenerator;
-use Mpociot\ApiDoc\Generators\LaravelGenerator;
-use Mpociot\ApiDoc\Generators\AbstractGenerator;
 
 class GenerateDocumentation extends Command
 {
@@ -47,15 +45,14 @@ class GenerateDocumentation extends Command
      */
     public function handle()
     {
-        $usingDIngoRouter = config('apidoc.router') == 'dingo';
-        if ($usingDIngoRouter) {
+        $usingDingoRouter = config('apidoc.router') == 'dingo';
+        if ($usingDingoRouter) {
             $routes = $this->routeMatcher->getDingoRoutesToBeDocumented(config('apidoc.routes'));
-            $generator = new DingoGenerator();
         } else {
             $routes = $this->routeMatcher->getLaravelRoutesToBeDocumented(config('apidoc.routes'));
-            $generator = new LaravelGenerator();
         }
 
+        $generator = new Generator();
         $parsedRoutes = $this->processRoutes($generator, $routes);
         $parsedRoutes = collect($parsedRoutes)->groupBy('group')
             ->sort(function ($a, $b) {
@@ -84,7 +81,7 @@ class GenerateDocumentation extends Command
 
         $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
             return $routeGroup->map(function ($route) {
-                $route['output'] = (string) view('apidoc::partials.route')->with('parsedRoute', $route)->render();
+                $route['output'] = (string) view('apidoc::partials.route')->with('route', $route)->render();
 
                 return $route;
             });
@@ -175,19 +172,19 @@ class GenerateDocumentation extends Command
     }
 
     /**
-     * @param AbstractGenerator $generator
+     * @param Generator $generator
      * @param array $routes
      *
      * @return array
      */
-    private function processRoutes(AbstractGenerator $generator, array $routes)
+    private function processRoutes(Generator $generator, array $routes)
     {
         $parsedRoutes = [];
         foreach ($routes as $routeItem) {
             $route = $routeItem['route'];
             /** @var Route $route */
             if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
-                $parsedRoutes[] = $generator->processRoute($route) + $routeItem['apply'];
+                $parsedRoutes[] = $generator->processRoute($route, $routeItem['apply']);
                 $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
             } else {
                 $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));

+ 5 - 7
src/Commands/UpdateDocumentation.php

@@ -12,16 +12,14 @@ class UpdateDocumentation extends Command
      *
      * @var string
      */
-    protected $signature = 'apidoc:update
-                            {--location=public/docs : The documentation location}
-    ';
+    protected $signature = 'apidoc:rebuild';
 
     /**
      * The console command description.
      *
      * @var string
      */
-    protected $description = 'Update and rebuild your API documentation from your markdown file.';
+    protected $description = 'Rebuild your API documentation from your markdown file.';
 
     /**
      * Create a new command instance.
@@ -40,16 +38,16 @@ class UpdateDocumentation extends Command
      */
     public function handle()
     {
-        $outputPath = $this->option('location');
+        $outputPath = config('apidoc.output');
 
         $documentarian = new Documentarian();
 
         if (! is_dir($outputPath)) {
-            $this->error('There is no generated documentation available at '.$outputPath.'.');
+            $this->error('There is no existing documentation available at '.$outputPath.'.');
 
             return false;
         }
-        $this->info('Updating API HTML code');
+        $this->info('Rebuilding API HTML code from '.$outputPath.'/source/index.md');
 
         $documentarian->generate($outputPath);
 

+ 0 - 412
src/Generators/AbstractGenerator.php

@@ -1,412 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Generators;
-
-use ReflectionClass;
-use Illuminate\Support\Str;
-use League\Fractal\Manager;
-use Illuminate\Routing\Route;
-use Mpociot\Reflection\DocBlock;
-use League\Fractal\Resource\Item;
-use Mpociot\Reflection\DocBlock\Tag;
-use League\Fractal\Resource\Collection;
-
-abstract class AbstractGenerator
-{
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getDomain(Route $route)
-    {
-        return $route->domain() == null ? '*' : $route->domain();
-    }
-
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getUri(Route $route)
-    {
-        return $route->uri();
-    }
-
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getMethods(Route $route)
-    {
-        return array_diff($route->methods(), ['HEAD']);
-    }
-
-    /**
-     * @param  \Illuminate\Routing\Route $route
-     * @param array $apply Rules to apply when generating documentation for this route
-     *
-     * @return array
-     */
-    public function processRoute($route)
-    {
-        $routeAction = $route->getAction();
-        $routeGroup = $this->getRouteGroup($routeAction['uses']);
-        $docBlock = $this->parseDocBlock($routeAction['uses']);
-        $content = $this->getResponse($docBlock['tags']);
-
-        return [
-            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
-            'group' => $routeGroup,
-            'title' => $docBlock['short'],
-            'description' => $docBlock['long'],
-            'methods' => $this->getMethods($route),
-            'uri' => $this->getUri($route),
-            'bodyParameters' => $this->getBodyParametersFromDocBlock($docBlock['tags']),
-            'queryParameters' => $this->getQueryParametersFromDocBlock($docBlock['tags']),
-            'response' => $content,
-            'showresponse' => ! empty($content),
-        ];
-    }
-
-    /**
-     * Prepares / Disables route middlewares.
-     *
-     * @param  bool $disable
-     *
-     * @return  void
-     */
-    abstract public function prepareMiddleware($enable = false);
-
-    /**
-     * Get the response from the docblock if available.
-     *
-     * @param array $tags
-     *
-     * @return mixed
-     */
-    protected function getDocblockResponse($tags)
-    {
-        $responseTags = array_filter($tags, function ($tag) {
-            return $tag instanceof Tag && \strtolower($tag->getName()) == 'response';
-        });
-        if (empty($responseTags)) {
-            return;
-        }
-        $responseTag = \array_first($responseTags);
-
-        return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']);
-    }
-
-    /**
-     * @param array $tags
-     *
-     * @return array
-     */
-    protected 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);
-                list($_, $name, $type, $required, $description) = $content;
-                $required = trim($required) == 'required' ? true : false;
-                $type = $this->normalizeParameterType($type);
-
-                return [$name => compact('type', 'description', 'required')];
-            })->toArray();
-
-        return $parameters;
-    }
-
-    /**
-     * @param array $tags
-     *
-     * @return array
-     */
-    protected 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);
-                list($_, $name, $required, $description) = $content;
-                $required = trim($required) == 'required' ? true : false;
-
-                return [$name => compact('description', 'required')];
-            })->toArray();
-
-        return $parameters;
-    }
-
-    /**
-     * @param  $route
-     * @param  $bindings
-     * @param  $headers
-     *
-     * @return \Illuminate\Http\Response
-     */
-    protected function getRouteResponse($route, $bindings, $headers = [])
-    {
-        $uri = $this->addRouteModelBindings($route, $bindings);
-
-        $methods = $this->getMethods($route);
-
-        // Split headers into key - value pairs
-        $headers = collect($headers)->map(function ($value) {
-            $split = explode(':', $value); // explode to get key + values
-            $key = array_shift($split); // extract the key and keep the values in the array
-            $value = implode(':', $split); // implode values into string again
-
-            return [trim($key) => trim($value)];
-        })->collapse()->toArray();
-
-        //Changes url with parameters like /users/{user} to /users/1
-        $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters
-
-        return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
-    }
-
-    /**
-     * @param $route
-     * @param array $bindings
-     *
-     * @return mixed
-     */
-    protected function addRouteModelBindings($route, $bindings)
-    {
-        $uri = $this->getUri($route);
-        foreach ($bindings as $model => $id) {
-            $uri = str_replace('{'.$model.'}', $id, $uri);
-            $uri = str_replace('{'.$model.'?}', $id, $uri);
-        }
-
-        return $uri;
-    }
-
-    /**
-     * @param  \Illuminate\Routing\Route  $route
-     *
-     * @return array
-     */
-    protected function parseDocBlock($route)
-    {
-        list($class, $method) = explode('@', $route);
-        $reflection = new ReflectionClass($class);
-        $reflectionMethod = $reflection->getMethod($method);
-
-        $comment = $reflectionMethod->getDocComment();
-        $phpdoc = new DocBlock($comment);
-
-        return [
-            'short' => $phpdoc->getShortDescription(),
-            'long' => $phpdoc->getLongDescription()->getContents(),
-            'tags' => $phpdoc->getTags(),
-        ];
-    }
-
-    /**
-     * @param  string  $route
-     *
-     * @return string
-     */
-    protected function getRouteGroup($route)
-    {
-        list($class, $method) = explode('@', $route);
-        $reflection = new ReflectionClass($class);
-        $comment = $reflection->getDocComment();
-        if ($comment) {
-            $phpdoc = new DocBlock($comment);
-            foreach ($phpdoc->getTags() as $tag) {
-                if ($tag->getName() === 'group') {
-                    return $tag->getContent();
-                }
-            }
-        }
-
-        return 'general';
-    }
-
-    /**
-     * Call the given URI and return the Response.
-     *
-     * @param  string  $method
-     * @param  string  $uri
-     * @param  array  $parameters
-     * @param  array  $cookies
-     * @param  array  $files
-     * @param  array  $server
-     * @param  string  $content
-     *
-     * @return \Illuminate\Http\Response
-     */
-    abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null);
-
-    /**
-     * Transform headers array to array of $_SERVER vars with HTTP_* format.
-     *
-     * @param  array  $headers
-     *
-     * @return array
-     */
-    protected function transformHeadersToServerVars(array $headers)
-    {
-        $server = [];
-        $prefix = 'HTTP_';
-
-        foreach ($headers as $name => $value) {
-            $name = strtr(strtoupper($name), '-', '_');
-
-            if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
-                $name = $prefix.$name;
-            }
-
-            $server[$name] = $value;
-        }
-
-        return $server;
-    }
-
-    /**
-     * @param $response
-     *
-     * @return mixed
-     */
-    private function getResponseContent($response)
-    {
-        if (empty($response)) {
-            return '';
-        }
-        if ($response->headers->get('Content-Type') === 'application/json') {
-            $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
-        } else {
-            $content = $response->getContent();
-        }
-
-        return $content;
-    }
-
-    /**
-     * Get a response from the transformer tags.
-     *
-     * @param array $tags
-     *
-     * @return mixed
-     */
-    protected function getTransformerResponse($tags)
-    {
-        try {
-            $transFormerTags = array_filter($tags, function ($tag) {
-                if (! ($tag instanceof Tag)) {
-                    return false;
-                }
-
-                return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']);
-            });
-            if (empty($transFormerTags)) {
-                // we didn't have any of the tags so goodbye
-                return false;
-            }
-
-            $modelTag = array_first(array_filter($tags, function ($tag) {
-                if (! ($tag instanceof Tag)) {
-                    return false;
-                }
-
-                return \in_array(\strtolower($tag->getName()), ['transformermodel']);
-            }));
-            $tag = \array_first($transFormerTags);
-            $transformer = $tag->getContent();
-            if (! \class_exists($transformer)) {
-                // if we can't find the transformer we can't generate a response
-                return;
-            }
-            $demoData = [];
-
-            $reflection = new ReflectionClass($transformer);
-            $method = $reflection->getMethod('transform');
-            $parameter = \array_first($method->getParameters());
-            $type = null;
-            if ($modelTag) {
-                $type = $modelTag->getContent();
-            }
-            if (\is_null($type)) {
-                if ($parameter->hasType() &&
-                    ! $parameter->getType()->isBuiltin() &&
-                    \class_exists((string) $parameter->getType())) {
-                    //we have a type
-                    $type = (string) $parameter->getType();
-                }
-            }
-            if ($type) {
-                // we have a class so we try to create an instance
-                $demoData = new $type;
-                try {
-                    // try a factory
-                    $demoData = \factory($type)->make();
-                } catch (\Exception $e) {
-                    if ($demoData instanceof \Illuminate\Database\Eloquent\Model) {
-                        // we can't use a factory but can try to get one from the database
-                        try {
-                            // check if we can find one
-                            $newDemoData = $type::first();
-                            if ($newDemoData) {
-                                $demoData = $newDemoData;
-                            }
-                        } catch (\Exception $e) {
-                            // do nothing
-                        }
-                    }
-                }
-            }
-
-            $fractal = new Manager();
-            $resource = [];
-            if ($tag->getName() == 'transformer') {
-                // just one
-                $resource = new Item($demoData, new $transformer);
-            }
-            if ($tag->getName() == 'transformercollection') {
-                // a collection
-                $resource = new Collection([$demoData, $demoData], new $transformer);
-            }
-
-            return \response($fractal->createData($resource)->toJson());
-        } catch (\Exception $e) {
-            // it isn't possible to parse the transformer
-            return;
-        }
-    }
-
-    private function getResponse(array $annotationTags)
-    {
-        $response = null;
-        if ($docblockResponse = $this->getDocblockResponse($annotationTags)) {
-            // we have a response from the docblock ( @response )
-            $response = $docblockResponse;
-        }
-        if (! $response && ($transformerResponse = $this->getTransformerResponse($annotationTags))) {
-            // we have a transformer response from the docblock ( @transformer || @transformercollection )
-            $response = $transformerResponse;
-        }
-
-        $content = $response ? $this->getResponseContent($response) : null;
-
-        return $content;
-    }
-
-    private function normalizeParameterType($type)
-    {
-        $typeMap = [
-            'int' => 'integer',
-            'bool' => 'boolean',
-        ];
-
-        return $type ? ($typeMap[$type] ?? $type) : 'string';
-    }
-}

+ 0 - 33
src/Generators/DingoGenerator.php

@@ -1,33 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Generators;
-
-class DingoGenerator extends AbstractGenerator
-{
-    /**
-     * Prepares / Disables route middlewares.
-     *
-     * @param  bool $disable
-     *
-     * @return  void
-     */
-    public function prepareMiddleware($disable = true)
-    {
-        // Not needed by Dingo
-        return false;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
-    {
-        $dispatcher = app('Dingo\Api\Dispatcher')->raw();
-
-        collect($server)->map(function ($key, $value) use ($dispatcher) {
-            $dispatcher->header($value, $key);
-        });
-
-        return call_user_func_array([$dispatcher, strtolower($method)], [$uri]);
-    }
-}

+ 247 - 0
src/Generators/Generator.php

@@ -0,0 +1,247 @@
+<?php
+
+namespace Mpociot\ApiDoc\Generators;
+
+use Faker\Factory;
+use ReflectionClass;
+use ReflectionMethod;
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock;
+use Mpociot\Reflection\DocBlock\Tag;
+use Mpociot\ApiDoc\Tools\ResponseResolver;
+
+class Generator
+{
+    /**
+     * @param Route $route
+     *
+     * @return mixed
+     */
+    public function getUri(Route $route)
+    {
+        return $route->uri();
+    }
+
+    /**
+     * @param Route $route
+     *
+     * @return mixed
+     */
+    public function getMethods(Route $route)
+    {
+        return array_diff($route->methods(), ['HEAD']);
+    }
+
+    /**
+     * @param  \Illuminate\Routing\Route $route
+     * @param array $apply Rules to apply when generating documentation for this route
+     *
+     * @return array
+     */
+    public function processRoute(Route $route, array $rulesToApply = [])
+    {
+        $routeAction = $route->getAction();
+        list($class, $method) = explode('@', $routeAction['uses']);
+        $controller = new ReflectionClass($class);
+        $method = $controller->getMethod($method);
+
+        $routeGroup = $this->getRouteGroup($controller, $method);
+        $docBlock = $this->parseDocBlock($method);
+        $content = ResponseResolver::getResponse($route, $docBlock['tags'], $rulesToApply);
+
+        $parsedRoute = [
+            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
+            'group' => $routeGroup,
+            'title' => $docBlock['short'],
+            'description' => $docBlock['long'],
+            'methods' => $this->getMethods($route),
+            'uri' => $this->getUri($route),
+            'bodyParameters' => $this->getBodyParametersFromDocBlock($docBlock['tags']),
+            'queryParameters' => $this->getQueryParametersFromDocBlock($docBlock['tags']),
+            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
+            'response' => $content,
+            'showresponse' => ! empty($content),
+        ];
+        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
+
+        return $parsedRoute;
+    }
+
+    /**
+     * @param array $tags
+     *
+     * @return array
+     */
+    protected function getBodyParametersFromDocBlock(array $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);
+                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);
+                $value = $this->generateDummyValue($type);
+
+                return [$name => compact('type', 'description', 'required', 'value')];
+            })->toArray();
+
+        return $parameters;
+    }
+
+    /**
+     * @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);
+                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;
+                }
+
+                return [$name => compact('description', 'required')];
+            })->toArray();
+
+        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 ReflectionMethod $method
+     *
+     * @return string
+     */
+    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
+    {
+        // @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 ($tag->getName() === 'group') {
+                    return $tag->getContent();
+                }
+            }
+        }
+
+        $docBlockComment = $controller->getDocComment();
+        if ($docBlockComment) {
+            $phpdoc = new DocBlock($docBlockComment);
+            foreach ($phpdoc->getTags() as $tag) {
+                if ($tag->getName() === 'group') {
+                    return $tag->getContent();
+                }
+            }
+        }
+
+        return 'general';
+    }
+
+    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();
+        $fakes = [
+            'integer' => function () {
+                return rand(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 str_random();
+            },
+            'array' => function () {
+                return '[]';
+            },
+            'object' => function () {
+                return '{}';
+            },
+        ];
+
+        $fake = $fakes[$type] ?? $fakes['string'];
+
+        return $fake();
+    }
+}

+ 0 - 85
src/Generators/LaravelGenerator.php

@@ -1,85 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Generators;
-
-use Illuminate\Routing\Route;
-use Illuminate\Support\Facades\App;
-use Illuminate\Support\Facades\Request;
-
-class LaravelGenerator extends AbstractGenerator
-{
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getUri(Route $route)
-    {
-        if (version_compare(app()->version(), '5.4', '<')) {
-            return $route->getUri();
-        }
-
-        return $route->uri();
-    }
-
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getMethods(Route $route)
-    {
-        if (version_compare(app()->version(), '5.4', '<')) {
-            $methods = $route->getMethods();
-        } else {
-            $methods = $route->methods();
-        }
-
-        return array_diff($methods, ['HEAD']);
-    }
-
-    /**
-     * Prepares / Disables route middlewares.
-     *
-     * @param  bool $disable
-     *
-     * @return  void
-     */
-    public function prepareMiddleware($enable = true)
-    {
-        App::instance('middleware.disable', ! $enable);
-    }
-
-    /**
-     * Call the given URI and return the Response.
-     *
-     * @param  string  $method
-     * @param  string  $uri
-     * @param  array  $parameters
-     * @param  array  $cookies
-     * @param  array  $files
-     * @param  array  $server
-     * @param  string  $content
-     *
-     * @return \Illuminate\Http\Response
-     */
-    public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
-    {
-        $server = collect([
-            'CONTENT_TYPE' => 'application/json',
-            'Accept' => 'application/json',
-        ])->merge($server)->toArray();
-
-        $request = Request::create(
-            $uri, $method, $parameters,
-            $cookies, $files, $this->transformHeadersToServerVars($server), $content
-        );
-
-        $kernel = App::make('Illuminate\Contracts\Http\Kernel');
-        $response = $kernel->handle($request);
-
-        $kernel->terminate($request, $response);
-
-        return $response;
-    }
-}

+ 54 - 0
src/Tools/ResponseResolver.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tools;
+
+use Illuminate\Routing\Route;
+use Mpociot\ApiDoc\Tools\ResponseStrategies\ResponseTagStrategy;
+use Mpociot\ApiDoc\Tools\ResponseStrategies\ResponseCallStrategy;
+use Mpociot\ApiDoc\Tools\ResponseStrategies\TransformerTagsStrategy;
+
+class ResponseResolver
+{
+    public static $strategies = [
+        ResponseTagStrategy::class,
+        TransformerTagsStrategy::class,
+        ResponseCallStrategy::class,
+    ];
+
+    /**
+     * @var Route
+     */
+    private $route;
+
+    public function __construct(Route $route)
+    {
+        $this->route = $route;
+    }
+
+    private function resolve(array $tags, array $rulesToApply)
+    {
+        $response = null;
+        foreach (static::$strategies as $strategy) {
+            $strategy = new $strategy();
+            $response = $strategy($this->route, $tags, $rulesToApply);
+            if (! is_null($response)) {
+                return $this->getResponseContent($response);
+            }
+        }
+    }
+
+    public static function getResponse($route, $tags, $rulesToApply)
+    {
+        return (new static($route))->resolve($tags, $rulesToApply);
+    }
+
+    /**
+     * @param $response
+     *
+     * @return mixed
+     */
+    private function getResponseContent($response)
+    {
+        return $response ? $response->getContent() : '';
+    }
+}

+ 244 - 0
src/Tools/ResponseStrategies/ResponseCallStrategy.php

@@ -0,0 +1,244 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
+
+use Dingo\Api\Dispatcher;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Routing\Route;
+
+/**
+ * Make a call to the route and retrieve its response.
+ */
+class ResponseCallStrategy
+{
+    public function __invoke(Route $route, array $tags, array $rulesToApply)
+    {
+        $rulesToApply = $rulesToApply['response_calls'] ?? [];
+        if (! $this->shouldMakeApiCall($route, $rulesToApply)) {
+            return;
+        }
+
+        $this->configureEnvironment($rulesToApply);
+        $request = $this->prepareRequest($route, $rulesToApply);
+        try {
+            $response = $this->makeApiCall($request);
+        } catch (\Exception $e) {
+            $response = null;
+        } finally {
+            $this->finish();
+        }
+
+        return $response;
+    }
+
+    private function configureEnvironment(array $rulesToApply)
+    {
+        $this->startDbTransaction();
+        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
+    }
+
+    private function prepareRequest(Route $route, array $rulesToApply)
+    {
+        $uri = $this->replaceUrlParameterBindings($route, $rulesToApply['bindings'] ?? []);
+        $routeMethods = $this->getMethods($route);
+        $method = array_shift($routeMethods);
+        $request = Request::create($uri, $method, [], [], [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? []));
+        $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []);
+        $request = $this->addQueryParameters($request, $rulesToApply['query'] ?? []);
+        $request = $this->addBodyParameters($request, $rulesToApply['body'] ?? []);
+
+        return $request;
+    }
+
+    /**
+     * Transform parameters in URLs into real values (/users/{user} -> /users/2).
+     * Uses bindings specified by caller, otherwise just uses '1'.
+     *
+     * @param Route $route
+     * @param array $bindings
+     *
+     * @return mixed
+     */
+    protected function replaceUrlParameterBindings(Route $route, $bindings)
+    {
+        $uri = $route->uri();
+        foreach ($bindings as $parameter => $binding) {
+            $uri = str_replace($parameter, $binding, $uri);
+        }
+        // Replace any unbound parameters with '1'
+        $uri = preg_replace('/{(.*?)}/', 1, $uri);
+
+        return $uri;
+    }
+
+    private function setEnvironmentVariables(array $env)
+    {
+        foreach ($env as $name => $value) {
+            putenv("$name=$value");
+
+            $_ENV[$name] = $value;
+            $_SERVER[$name] = $value;
+        }
+    }
+
+    private function startDbTransaction()
+    {
+        try {
+            app('db')->beginTransaction();
+        } catch (\Exception $e) {
+        }
+    }
+
+    private function endDbTransaction()
+    {
+        try {
+            app('db')->rollBack();
+        } catch (\Exception $e) {
+        }
+    }
+
+    private function finish()
+    {
+        $this->endDbTransaction();
+    }
+
+    public function callDingoRoute(Request $request)
+    {
+        /** @var Dispatcher $dispatcher */
+        $dispatcher = app(\Dingo\Api\Dispatcher::class);
+
+        foreach ($request->headers as $header => $value) {
+            $dispatcher->header($header, $value);
+        }
+
+        // set domain and body parameters
+        $dispatcher->on($request->header('SERVER_NAME'))
+            ->with($request->request->all());
+
+        // set URL and query parameters
+        $uri = $request->getRequestUri();
+        $query = $request->getQueryString();
+        if (! empty($query)) {
+            $uri .= "?$query";
+        }
+        $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
+
+        // the response from the Dingo dispatcher is the 'raw' response from the controller,
+        // so we have to ensure it's JSON first
+        if (! $response instanceof Response) {
+            $response = response()->json($response);
+        }
+
+        return $response;
+    }
+
+    public function getMethods(Route $route)
+    {
+        return array_diff($route->methods(), ['HEAD']);
+    }
+
+    private function addHeaders(Request $request, Route $route, $headers)
+    {
+        // set the proper domain
+        if ($route->getDomain()) {
+            $request->server->add([
+                'HTTP_HOST' => $route->getDomain(),
+                'SERVER_NAME' => $route->getDomain(),
+            ]);
+        }
+
+        $headers = collect($headers);
+
+        if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
+            $request->setRequestFormat('json');
+        }
+
+        return $request;
+    }
+
+    private function addQueryParameters(Request $request, array $query)
+    {
+        $request->query->add($query);
+        $request->server->add(['QUERY_STRING' => http_build_query($query)]);
+
+        return $request;
+    }
+
+    private function addBodyParameters(Request $request, array $body)
+    {
+        $request->request->add($body);
+
+        return $request;
+    }
+
+    private function makeApiCall(Request $request)
+    {
+        if (config('apidoc.router') == 'dingo') {
+            $response = $this->callDingoRoute($request);
+        } else {
+            $response = $this->callLaravelRoute($request);
+        }
+
+        return $response;
+    }
+
+    /**
+     * @param $request
+     *
+     * @return \Symfony\Component\HttpFoundation\Response
+     */
+    private function callLaravelRoute($request): \Symfony\Component\HttpFoundation\Response
+    {
+        $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
+        $response = $kernel->handle($request);
+        $kernel->terminate($request, $response);
+
+        return $response;
+    }
+
+    private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
+    {
+        $allowedMethods = $rulesToApply['methods'] ?? [];
+        if (empty($allowedMethods)) {
+            return false;
+        }
+
+        if (is_string($allowedMethods) && $allowedMethods == '*') {
+            return true;
+        }
+
+        if (array_search('*', $allowedMethods) !== false) {
+            return true;
+        }
+
+        $routeMethods = $this->getMethods($route);
+        if (in_array(array_shift($routeMethods), $allowedMethods)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Transform headers array to array of $_SERVER vars with HTTP_* format.
+     *
+     * @param  array  $headers
+     *
+     * @return array
+     */
+    protected function transformHeadersToServerVars(array $headers)
+    {
+        $server = [];
+        $prefix = 'HTTP_';
+        foreach ($headers as $name => $value) {
+            $name = strtr(strtoupper($name), '-', '_');
+            if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') {
+                $name = $prefix.$name;
+            }
+            $server[$name] = $value;
+        }
+
+        return $server;
+    }
+}

+ 37 - 0
src/Tools/ResponseStrategies/ResponseTagStrategy.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
+
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock\Tag;
+
+/**
+ * Get a response from the docblock ( @response ).
+ */
+class ResponseTagStrategy
+{
+    public function __invoke(Route $route, array $tags, array $rulesToApply)
+    {
+        return $this->getDocBlockResponse($tags);
+    }
+
+    /**
+     * Get the response from the docblock if available.
+     *
+     * @param array $tags
+     *
+     * @return mixed
+     */
+    protected function getDocBlockResponse(array $tags)
+    {
+        $responseTags = array_filter($tags, function ($tag) {
+            return $tag instanceof Tag && strtolower($tag->getName()) == 'response';
+        });
+        if (empty($responseTags)) {
+            return;
+        }
+        $responseTag = array_first($responseTags);
+
+        return response()->json(json_decode($responseTag->getContent(), true));
+    }
+}

+ 129 - 0
src/Tools/ResponseStrategies/TransformerTagsStrategy.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
+
+use ReflectionClass;
+use ReflectionMethod;
+use League\Fractal\Manager;
+use Illuminate\Routing\Route;
+use League\Fractal\Resource\Item;
+use Mpociot\Reflection\DocBlock\Tag;
+use League\Fractal\Resource\Collection;
+
+/**
+ * Parse a transformer response from the docblock ( @transformer || @transformercollection ).
+ */
+class TransformerTagsStrategy
+{
+    public function __invoke(Route $route, array $tags, array $rulesToApply)
+    {
+        return $this->getTransformerResponse($tags);
+    }
+
+    /**
+     * Get a response from the transformer tags.
+     *
+     * @param array $tags
+     *
+     * @return mixed
+     */
+    protected function getTransformerResponse(array $tags)
+    {
+        try {
+            if (empty($transformerTag = $this->getTransformerTag($tags))) {
+                return;
+            }
+
+            $transformer = $this->getTransformerClass($transformerTag);
+            $model = $this->getClassToBeTransformed($tags, (new ReflectionClass($transformer))->getMethod('transform'));
+            $modelInstance = $this->instantiateTransformerModel($model);
+
+            $fractal = new Manager();
+            $resource = (strtolower($transformerTag->getName()) == 'transformercollection')
+                ? new Collection([$modelInstance, $modelInstance], new $transformer)
+                : new Item($modelInstance, new $transformer);
+
+            return response($fractal->createData($resource)->toJson());
+        } catch (\Exception $e) {
+            return;
+        }
+    }
+
+    /**
+     * @param Tag $tag
+     *
+     * @return string|null
+     */
+    private function getTransformerClass($tag)
+    {
+        return $tag->getContent();
+    }
+
+    /**
+     * @param array $tags
+     * @param ReflectionMethod $transformerMethod
+     *
+     * @return null|string
+     */
+    private function getClassToBeTransformed(array $tags, ReflectionMethod $transformerMethod)
+    {
+        $modelTag = array_first(array_filter($tags, function ($tag) {
+            return ($tag instanceof Tag) && strtolower($tag->getName()) == 'transformermodel';
+        }));
+
+        $type = null;
+        if ($modelTag) {
+            $type = $modelTag->getContent();
+        } else {
+            $parameter = array_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();
+            }
+        }
+
+        return $type;
+    }
+
+    /**
+     * @param string $type
+     *
+     * @return mixed
+     */
+    protected function instantiateTransformerModel(string $type)
+    {
+        try {
+            // try Eloquent model factory
+            return factory($type)->make();
+        } catch (\Exception $e) {
+            $instance = new $type;
+            if ($instance instanceof \Illuminate\Database\Eloquent\Model) {
+                try {
+                    // we can't use a factory but can try to get one from the database
+                    $firstInstance = $type::first();
+                    if ($firstInstance) {
+                        return $firstInstance;
+                    }
+                } catch (\Exception $e) {
+                    // okay, we'll stick with `new`
+                }
+            }
+        }
+
+        return $instance;
+    }
+
+    /**
+     * @param array $tags
+     *
+     * @return Tag|null
+     */
+    private function getTransformerTag(array $tags)
+    {
+        $transFormerTags = array_filter($tags, function ($tag) {
+            return ($tag instanceof Tag) && in_array(strtolower($tag->getName()), ['transformer', 'transformercollection']);
+        });
+
+        return array_first($transFormerTags);
+    }
+}

+ 45 - 11
tests/Fixtures/TestController.php

@@ -5,6 +5,9 @@ namespace Mpociot\ApiDoc\Tests\Fixtures;
 use Illuminate\Http\Request;
 use Illuminate\Routing\Controller;
 
+/**
+ * @group Group A
+ */
 class TestController extends Controller
 {
     public function dummy()
@@ -22,9 +25,21 @@ class TestController extends Controller
         return '';
     }
 
+    /**
+     * @group Group B
+     */
+    public function withGroupOverride()
+    {
+        return '';
+    }
+
     /**
      * @bodyParam user_id int required The id of the user.
      * @bodyParam room_id string The id of the room.
+     * @bodyParam forever boolean Whether to ban the user forever.
+     * @bodyParam another_one number Just need something here.
+     * @bodyParam yet_another_param object required
+     * @bodyParam even_more_param array
      */
     public function withBodyParameters()
     {
@@ -40,6 +55,14 @@ class TestController extends Controller
         return '';
     }
 
+    /**
+     * @authenticated
+     */
+    public function withAuthenticatedTag()
+    {
+        return '';
+    }
+
     public function checkCustomHeaders(Request $request)
     {
         return $request->headers->all();
@@ -47,19 +70,30 @@ class TestController extends Controller
 
     public function shouldFetchRouteResponse()
     {
-        $fixture = new \stdClass();
-        $fixture->id = 1;
-        $fixture->name = 'banana';
-        $fixture->color = 'red';
-        $fixture->weight = 300;
-        $fixture->delicious = 1;
+        $fruit = new \stdClass();
+        $fruit->id = 4;
+        $fruit->name = ' banana  ';
+        $fruit->color = 'RED';
+        $fruit->weight = 1;
+        $fruit->delicious = true;
+
+        return [
+            'id' => (int) $fruit->id,
+            'name' => trim($fruit->name),
+            'color' => strtolower($fruit->color),
+            'weight' => $fruit->weight.' kg',
+            'delicious' => $fruit->delicious,
+        ];
+    }
 
+    public function shouldFetchRouteResponseWithEchoedSettings($id)
+    {
         return [
-            'id' => (int) $fixture->id,
-            'name' => ucfirst($fixture->name),
-            'color' => ucfirst($fixture->color),
-            'weight' => $fixture->weight.' grams',
-            'delicious' => (bool) $fixture->delicious,
+            '{id}' => $id,
+            'APP_ENV' => getenv('APP_ENV'),
+            'header' => request()->header('header'),
+            'queryParam' => request()->query('queryParam'),
+            'bodyParam' => request()->get('bodyParam'),
         ];
     }
 

+ 0 - 9
tests/Fixtures/TestResourceController.php

@@ -48,9 +48,6 @@ class TestResourceController extends Controller
      */
     public function store(Request $request)
     {
-        return [
-            'store_resource' => true,
-        ];
     }
 
     /**
@@ -99,9 +96,6 @@ class TestResourceController extends Controller
      */
     public function update(Request $request, $id)
     {
-        return [
-            'update_resource' => $id,
-        ];
     }
 
     /**
@@ -113,8 +107,5 @@ class TestResourceController extends Controller
      */
     public function destroy($id)
     {
-        return [
-            'destroy_resource' => $id,
-        ];
     }
 }

+ 1 - 1
tests/Fixtures/collection.json

@@ -1 +1 @@
-{"variables":[],"info":{"name":"","_postman_id":"","description":"","schema":"https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json"},"item":[{"name":"general","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":"","_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":[]}}]}]}

+ 140 - 15
tests/Fixtures/index.md

@@ -21,27 +21,31 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_0bef4e738c9d6720ad43b062015d1078 -->
+<!-- START_264ee15c728df32e7ca6eedce5e42dcb -->
 ## Example title.
-
+ 
 This will be the long description.
 It can also be multiple lines long.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/test" \
-    -H "Accept: application/json"
+curl -X GET -G "http://localhost/api/withDescription" \
+    -H "Accept: application/json" \
+    -H "Authorization: customAuthToken" \
+        -H "Custom-Header: NotSoCustom" 
 ```
 
 ```javascript
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/test",
+    "url": "http://localhost/api/withDescription",
     "method": "GET",
     "headers": {
         "accept": "application/json",
+        "Authorization": "customAuthToken",
+        "Custom-Header": "NotSoCustom",
     }
 }
 
@@ -57,29 +61,33 @@ null
 ```
 
 ### HTTP Request
-`GET api/test`
-
+`GET api/withDescription`
 
-<!-- END_0bef4e738c9d6720ad43b062015d1078 -->
 
-<!-- START_39a6bfda1d6a0c4a5447f51b62557456 -->
-## api/responseTag
+<!-- END_264ee15c728df32e7ca6eedce5e42dcb -->
 
+<!-- START_9cedd363be06f5512f9e844b100fcc9d -->
+## api/withResponseTag
+ 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/responseTag" \
-    -H "Accept: application/json"
+curl -X GET -G "http://localhost/api/withResponseTag" \
+    -H "Accept: application/json" \
+    -H "Authorization: customAuthToken" \
+        -H "Custom-Header: NotSoCustom" 
 ```
 
 ```javascript
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/responseTag",
+    "url": "http://localhost/api/withResponseTag",
     "method": "GET",
     "headers": {
         "accept": "application/json",
+        "Authorization": "customAuthToken",
+        "Custom-Header": "NotSoCustom",
     }
 }
 
@@ -101,9 +109,126 @@ $.ajax(settings).done(function (response) {
 ```
 
 ### HTTP Request
-`GET api/responseTag`
+`GET api/withResponseTag`
+
+
+<!-- END_9cedd363be06f5512f9e844b100fcc9d -->
+
+<!-- START_a25cb3b490fa579d7d77b386bbb7ec03 -->
+## 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"=[] 
+```
+
+```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": "[]"
+    },
+    "headers": {
+        "accept": "application/json",
+        "Authorization": "customAuthToken",
+        "Custom-Header": "NotSoCustom",
+    }
+}
+
+$.ajax(settings).done(function (response) {
+    console.log(response);
+});
+```
+
+> Example response:
+
+```json
+null
+```
+
+### HTTP Request
+`GET api/withBodyParameters`
+
+#### Parameters
+
+Parameter | Type | Status | Description
+--------- | ------- | ------- | ------- | -----------
+    user_id | integer |  required  | The id of the user.
+    room_id | string |  optional  | The id of the room.
+    forever | boolean |  optional  | Whether to ban the user forever.
+    another_one | number |  optional  | Just need something here.
+    yet_another_param | object |  required  | 
+    even_more_param | array |  optional  | 
+
+<!-- END_a25cb3b490fa579d7d77b386bbb7ec03 -->
+
+<!-- START_5c08cc4d72b6e5830f6814c64086e197 -->
+## api/withAuthTag
+ <small style="
+  padding: 1px 9px 2px;
+  font-weight: bold;
+  white-space: nowrap;
+  color: #ffffff;
+  -webkit-border-radius: 9px;
+  -moz-border-radius: 9px;
+  border-radius: 9px;
+  background-color: #3a87ad;">Requires authentication</small>
+
+> Example request:
+
+```bash
+curl -X GET -G "http://localhost/api/withAuthTag" \
+    -H "Accept: application/json" \
+    -H "Authorization: customAuthToken" \
+        -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",
+    }
+}
+
+$.ajax(settings).done(function (response) {
+    console.log(response);
+});
+```
+
+> Example response:
+
+```json
+null
+```
+
+### HTTP Request
+`GET api/withAuthTag`
 
 
-<!-- END_39a6bfda1d6a0c4a5447f51b62557456 -->
+<!-- END_5c08cc4d72b6e5830f6814c64086e197 -->
 
 

+ 12 - 2
tests/GenerateDocumentationTest.php

@@ -155,15 +155,25 @@ class GenerateDocumentationTest extends TestCase
     /** @test */
     public function generated_markdown_file_is_correct()
     {
-        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
-        RouteFacade::get('/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/withAuthTag', TestController::class.'@withAuthenticatedTag');
 
         config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        config([
+            'apidoc.routes.0.apply.headers' => [
+                'Authorization' => 'customAuthToken',
+                'Custom-Header' => 'NotSoCustom',
+            ],
+        ]);
         $this->artisan('apidoc:generate');
 
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
         $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);
     }

+ 3 - 4
tests/Unit/DingoGeneratorTest.php

@@ -3,7 +3,6 @@
 namespace Mpociot\ApiDoc\Tests\Unit;
 
 use Dingo\Api\Routing\Router;
-use Mpociot\ApiDoc\Generators\DingoGenerator;
 use Mpociot\ApiDoc\Tests\Fixtures\TestController;
 use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
 
@@ -12,8 +11,8 @@ class DingoGeneratorTest extends GeneratorTestCase
     protected function getPackageProviders($app)
     {
         return [
-            \Dingo\Api\Provider\LaravelServiceProvider::class,
             ApiDocGeneratorServiceProvider::class,
+            \Dingo\Api\Provider\LaravelServiceProvider::class,
         ];
     }
 
@@ -21,10 +20,10 @@ class DingoGeneratorTest extends GeneratorTestCase
     {
         parent::setUp();
 
-        $this->generator = new DingoGenerator();
+        config(['apidoc.router' => 'dingo']);
     }
 
-    public function createRoute(string $httpMethod, string $path, string $controllerMethod)
+    public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false)
     {
         $route = null;
         /** @var Router $api */

+ 133 - 15
tests/Unit/GeneratorTestCase.php

@@ -3,13 +3,13 @@
 namespace Mpociot\ApiDoc\Tests\Unit;
 
 use Orchestra\Testbench\TestCase;
-use Mpociot\ApiDoc\Generators\LaravelGenerator;
+use Mpociot\ApiDoc\Generators\Generator;
 use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
 
 abstract class GeneratorTestCase extends TestCase
 {
     /**
-     * @var \Mpociot\ApiDoc\Generators\AbstractGenerator
+     * @var \Mpociot\ApiDoc\Generators\Generator
      */
     protected $generator;
 
@@ -27,11 +27,11 @@ abstract class GeneratorTestCase extends TestCase
     {
         parent::setUp();
 
-        $this->generator = new LaravelGenerator();
+        $this->generator = new Generator();
     }
 
     /** @test */
-    public function test_can_parse_endpoint_description()
+    public function can_parse_endpoint_description()
     {
         $route = $this->createRoute('GET', '/api/test', 'withEndpointDescription');
         $parsed = $this->generator->processRoute($route);
@@ -41,7 +41,7 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_body_parameters()
+    public function can_parse_body_parameters()
     {
         $route = $this->createRoute('GET', '/api/test', 'withBodyParameters');
         $bodyParameters = $this->generator->processRoute($route)['bodyParameters'];
@@ -57,11 +57,31 @@ abstract class GeneratorTestCase extends TestCase
                 'required' => false,
                 'description' => 'The id of the room.',
             ],
+            'forever' => [
+                'type' => 'boolean',
+                'required' => false,
+                'description' => 'Whether to ban the user forever.',
+            ],
+            'another_one' => [
+                'type' => 'number',
+                'required' => false,
+                'description' => 'Just need something here.',
+            ],
+            'yet_another_param' => [
+                'type' => 'object',
+                'required' => true,
+                'description' => '',
+            ],
+            'even_more_param' => [
+                'type' => 'array',
+                'required' => false,
+                'description' => '',
+            ],
         ], $bodyParameters);
     }
 
     /** @test */
-    public function test_can_parse_query_parameters()
+    public function can_parse_query_parameters()
     {
         $route = $this->createRoute('GET', '/api/test', 'withQueryParameters');
         $queryParameters = $this->generator->processRoute($route)['queryParameters'];
@@ -79,7 +99,37 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_route_methods()
+    public function can_parse_route_group()
+    {
+        $route = $this->createRoute('GET', '/api/test', 'dummy');
+        $routeGroup = $this->generator->processRoute($route)['group'];
+
+        $this->assertSame('Group A', $routeGroup);
+    }
+
+    /** @test */
+    public function method_can_override_controller_group()
+    {
+        $route = $this->createRoute('GET', '/api/test', 'withGroupOverride');
+        $routeGroup = $this->generator->processRoute($route)['group'];
+
+        $this->assertSame('Group B', $routeGroup);
+    }
+
+    /** @test */
+    public function can_parse_auth_tags()
+    {
+        $route = $this->createRoute('GET', '/api/test', 'withAuthenticatedTag');
+        $authenticated = $this->generator->processRoute($route)['authenticated'];
+        $this->assertTrue($authenticated);
+
+        $route = $this->createRoute('GET', '/api/test', 'dummy');
+        $authenticated = $this->generator->processRoute($route)['authenticated'];
+        $this->assertFalse($authenticated);
+    }
+
+    /** @test */
+    public function can_parse_route_methods()
     {
         $route = $this->createRoute('GET', '/get', 'withEndpointDescription');
         $parsed = $this->generator->processRoute($route);
@@ -99,7 +149,7 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_response_tag()
+    public function can_parse_response_tag()
     {
         $route = $this->createRoute('POST', '/responseTag', 'withResponseTag');
 
@@ -108,17 +158,17 @@ abstract class GeneratorTestCase extends TestCase
         $this->assertTrue(is_array($parsed));
         $this->assertArrayHasKey('showresponse', $parsed);
         $this->assertTrue($parsed['showresponse']);
-        $this->assertJsonStringEqualsJsonString(json_encode([
+        $this->assertArraySubset([
             'id' => 4,
             'name' => 'banana',
             'color' => 'red',
             'weight' => '1 kg',
             'delicious' => true,
-        ]), $parsed['response']);
+        ], json_decode($parsed['response'], true));
     }
 
     /** @test */
-    public function test_can_parse_transformer_tag()
+    public function can_parse_transformer_tag()
     {
         $route = $this->createRoute('GET', '/transformerTag', 'transformerTag');
         $parsed = $this->generator->processRoute($route);
@@ -132,7 +182,7 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_transformer_tag_with_model()
+    public function can_parse_transformer_tag_with_model()
     {
         $route = $this->createRoute('GET', '/transformerTagWithModel', 'transformerTagWithModel');
         $parsed = $this->generator->processRoute($route);
@@ -146,7 +196,7 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_transformer_collection_tag()
+    public function can_parse_transformer_collection_tag()
     {
         $route = $this->createRoute('GET', '/transformerCollectionTag', 'transformerCollectionTag');
         $parsed = $this->generator->processRoute($route);
@@ -161,7 +211,7 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function test_can_parse_transformer_collection_tag_with_model()
+    public function can_parse_transformer_collection_tag_with_model()
     {
         $route = $this->createRoute('GET', '/transformerCollectionTagWithModel', 'transformerCollectionTagWithModel');
         $parsed = $this->generator->processRoute($route);
@@ -175,5 +225,73 @@ abstract class GeneratorTestCase extends TestCase
         );
     }
 
-    abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod);
+    /** @test */
+    public function can_call_route_and_generate_response()
+    {
+        $route = $this->createRoute('PUT', '/shouldFetchRouteResponse', 'shouldFetchRouteResponse', true);
+
+        $rules = [
+            'response_calls' => [
+                'methods' => ['*'],
+                'headers' => [
+                    'Content-Type' => 'application/json',
+                    'Accept' => 'application/json',
+                ],
+            ],
+        ];
+        $parsed = $this->generator->processRoute($route, $rules);
+
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertArraySubset([
+            'id' => 4,
+            'name' => 'banana',
+            'color' => 'red',
+            'weight' => '1 kg',
+            'delicious' => true,
+        ], json_decode($parsed['response'], true));
+    }
+
+    /** @test */
+    public function uses_configured_settings_when_calling_route()
+    {
+        $route = $this->createRoute('PUT', '/echo/{id}', 'shouldFetchRouteResponseWithEchoedSettings', true);
+
+        $rules = [
+            'response_calls' => [
+                'methods' => ['*'],
+                'headers' => [
+                    'Content-Type' => 'application/json',
+                    'Accept' => 'application/json',
+                    'header' => 'value',
+                ],
+                'bindings' => [
+                    '{id}' => 3,
+                ],
+                'env' => [
+                    'APP_ENV' => 'documentation',
+                ],
+                'query' => [
+                    'queryParam' => 'queryValue',
+                ],
+                'body' => [
+                    'bodyParam' => 'bodyValue',
+                ],
+            ],
+        ];
+        $parsed = $this->generator->processRoute($route, $rules);
+
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $response = json_decode($parsed['response'], true);
+        $this->assertEquals(3, $response['{id}']);
+        $this->assertEquals('documentation', $response['APP_ENV']);
+        $this->assertEquals('queryValue', $response['queryParam']);
+        $this->assertEquals('bodyValue', $response['bodyParam']);
+        $this->assertEquals('value', $response['header']);
+    }
+
+    abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false);
 }

+ 7 - 10
tests/Unit/LaravelGeneratorTest.php

@@ -3,9 +3,9 @@
 namespace Mpociot\ApiDoc\Tests\Unit;
 
 use Illuminate\Routing\Route;
-use Mpociot\ApiDoc\Generators\LaravelGenerator;
 use Mpociot\ApiDoc\Tests\Fixtures\TestController;
 use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
+use Illuminate\Support\Facades\Route as RouteFacade;
 
 class LaravelGeneratorTest extends GeneratorTestCase
 {
@@ -16,15 +16,12 @@ class LaravelGeneratorTest extends GeneratorTestCase
         ];
     }
 
-    public function setUp()
+    public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false)
     {
-        parent::setUp();
-
-        $this->generator = new LaravelGenerator();
-    }
-
-    public function createRoute(string $httpMethod, string $path, string $controllerMethod)
-    {
-        return new Route([$httpMethod], $path, ['uses' => TestController::class."@$controllerMethod"]);
+        if ($register) {
+            return RouteFacade::{$httpMethod}($path, TestController::class."@$controllerMethod");
+        } else {
+            return new Route([$httpMethod], $path, ['uses' => TestController::class."@$controllerMethod"]);
+        }
     }
 }