Bläddra i källkod

Merge pull request #362 from shalvah/v3

V3 - first working build
Shalvah A 6 år sedan
förälder
incheckning
9528d5fb92

+ 7 - 1
.travis.yml

@@ -1,12 +1,18 @@
 language: php
 
 php:
+  - 7.0.0
   - 7.1.3
   - 7.2
 
+env:
+  matrix:
+    - PREFER_LOWEST="--prefer-lowest"
+    - PREFER_LOWEST=""
+
 before_script:
   - travis_retry composer self-update
-  - travis_retry composer install --prefer-dist --no-interaction
+  - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST
 
 script:
   - composer test-ci

+ 142 - 112
README.md

@@ -2,7 +2,7 @@
 
 Automatically generate your API documentation from your existing Laravel routes. Take a look at the [example documentation](http://marcelpociot.de/whiteboard/).
 
-`php artisan apidoc:gen --routePrefix="settings/api/*"`
+`php artisan apidoc:generate`
 
 [![Latest Stable Version](https://poser.pugx.org/mpociot/laravel-apidoc-generator/v/stable)](https://packagist.org/packages/mpociot/laravel-apidoc-generator)[![Total Downloads](https://poser.pugx.org/mpociot/laravel-apidoc-generator/downloads)](https://packagist.org/packages/mpociot/laravel-apidoc-generator)
 [![License](https://poser.pugx.org/mpociot/laravel-apidoc-generator/license)](https://packagist.org/packages/mpociot/laravel-apidoc-generator)
@@ -13,8 +13,7 @@ Automatically generate your API documentation from your existing Laravel routes.
 
 
 ## Installation
-
-Require this package with composer using the following command:
+> Note: version 3.x requires PHP 7 and Laravel 5.5 or higher. Version 2.x requires Laravel 5.4.
 
 ```sh
 $ composer require mpociot/laravel-apidoc-generator
@@ -25,70 +24,112 @@ Using Laravel < 5.5? Go to your `config/app.php` and add the service provider:
 Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class,
 ```
 
-> Using Laravel < 5.4? Use version 1.0! For Laravel 5.4 and up, use 2.0 instead.
+Then publish the config file by running:
+
+```bash
+php artisan vendor:publish --provider=Mpociot\ApiDoc\ApiDocGeneratorServiceProvider --tag=config
+```
+This will create an `apidoc.php` file in your `config` folder.
 
 ## Usage
+Before you can generate your documentation, you'll need to configure a few things in your `config/apidoc.php`.
+### output
+This is the file path where the generated documentation will be written to. Default: `**public/docs**
 
-To generate your API documentation, use the `apidoc:generate` artisan command.
+### postman
+Set this option to true if you want a Postman collection to be generated along with the documentation. Default: `**true**
 
-```sh
-$ php artisan apidoc:generate --routePrefix="api/v1/*"
+### router
+The router to use when processing the route (can be Laravel or Dingo. Defaults to **Laravel**)
 
-```
-You can pass in multiple prefixes by spearating each prefix with comma.
+### routes
+This is where you specify what rules documentation should be generated for. You specify routes to be parsed by defining conditions that the routes should meet and rules that should be applied when generating documentation. These conditions and rules are specified in groups, allowing you to apply different rules to different routes.
 
-```sh
-$ php artisan apidoc:generate --routePrefix="api/v1/*,api/public/*"
+For instance, suppose your configuration looks like this:
+
+```php
+<?php
+return [
+     //...,
+  
+     'routes' => [
+          [
+              'match' => [
+                  'domains' => ['*'],
+                  'prefixes' => ['api/*', 'v2-api/*'],
+                  'versions' => ['v1'],
+              ],
+              'include' => ['users.index'],
+              'exclude' => ['users.create'],
+              'apply' => [
+                  'headers' => [
+                      'Authorization' => 'Bearer: {token}',
+                  ],
+              ],
+          ],
+];
 ```
-It will generate documentation for all of the routes whose prefixes are `api/v1/` and `api/public/`
 
+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).
+Also, in the generated documentation, these routes will have the header 'Authorization: Bearer: {token}' added to the example requests.
 
-This command will scan your applications routes for the URIs matching `api/v1/*` and will parse these controller methods and form requests. For example:
+You can also separate routes into groups to apply different rules to them:
 
 ```php
-// API Group Routes
-Route::group(array('prefix' => 'api/v1', 'middleware' => []), function () {
-	// Custom route added to standard Resource
-	Route::get('example/foo', 'ExampleController@foo');
-	// Standard Resource route
-	Route::resource('example', 'ExampleController');
-});
+<?php
+return [
+     //...,
+  
+     'routes' => [
+          [
+              'match' => [
+                  'domains' => ['v1.*'],
+                  'prefixes' => ['*'],
+              ],
+              'include' => [],
+              'exclude' => [],
+              'apply' => [
+                  'headers' => [
+                      'Token' => '{token}',
+                      'Version' => 'v1',
+                  ],
+              ],
+          ],
+          [
+              'match' => [
+                  'domains' => ['v2.*'],
+                  'prefixes' => ['*'],
+              ],
+              'include' => [],
+              'exclude' => [],
+              'apply' => [
+                  'headers' => [
+                      'Authorization' => 'Bearer: {token}',
+                      'Api-Version' => 'v2',
+                  ],
+              ],
+          ],
+];
 ```
 
-### Available command options:
-
-Option | Description
---------- | -------
-`output` | The output path used for the generated documentation. Default: `public/docs`
-`routePrefix` | The route prefix(es) to use for generation. `*` can be used as a wildcard. Multiple route prefixes can be specified by separating them with a comma (for instance `/v1,/v2`)
-`routeDomain` | The route domain(s) to use for generation. `*` can be used as a wildcard. Multiple route domains can be specified by separating them with a comma 
-`routes` | The route names to use for generation - Required if no routePrefix or routeDomain is provided
-`middleware` | The middlewares to use for generation
-`noResponseCalls` | Disable API response calls
-`noPostmanCollection` | Disable Postman collection creation
-`useMiddlewares` | Use all configured route middlewares (Needed for Laravel 5.3 `SubstituteBindings` middleware)
-`actAsUserId` | The user ID to use for authenticated API response calls
-`router` | The router to use, when processing the route files (can be Laravel or Dingo - defaults to Laravel)
-`bindings` | List of route bindings that should be replaced when trying to retrieve route results. Syntax format: `binding_one,id|binding_two,id`
-`force` | Force the re-generation of existing/modified API routes
-`header` | Custom HTTP headers to add to the example requests. Separate the header name and value with ":". For example: `--header="Authorization: CustomToken"`
+With the configuration above, routes on the `v1.*` domain will have the `Token` and `Version` headers applied, while routes on the `v2.*` domain will have the `Authorization` and `Api-Version` headers applied.
 
-## Publish rule descriptions for customisation or translation.
+> Note: If you're using DIngo router, the `versions` parameter is required in each route group. This parameter does not support wildcards. Each version must be listed explicitly,
 
- By default, this package returns the descriptions in english. You can publish the packages language files, to customise and translate the documentation output.
+To generate your API documentation, use the `apidoc:generate` artisan command.
 
- ```sh
- $ php artisan vendor:publish
- ```
+```sh
+$ php artisan apidoc:generate
 
- After the files are published you can customise or translate the descriptions in the language you want by renaming the `en` folder and editing the files in `public/vendor/apidoc/resources/lang`.
+```
 
+It will generate documentation using your specified configuration.
 
-### How does it work?
+## Documenting your API
 
 This package uses these resources to generate the API documentation:
 
-#### Controller doc block
+### Grouping endpoints
 
 This package uses the HTTP controller doc blocks to create a table of contents and show descriptions for your API methods.
 
@@ -119,106 +160,97 @@ class ExampleController extends Controller {
 
 ![Doc block result](http://headsquaredsoftware.co.uk/images/api_generator_docblock.png)
 
-#### Form request validation rules
+### Specifying request body parameters
 
-To display a list of valid parameters, your API methods accepts, this package uses Laravel's [Form Requests Validation](https://laravel.com/docs/5.2/validation#form-request-validation).
+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
 
 
 ```php
-public function rules()
+/**
+ * @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 thumbnail image This is required if the post type is 'imagelicious'.
+ */
+public function createPost()
 {
-    return [
-        'title' => 'required|max:255',
-        'body' => 'required',
-        'type' => 'in:foo,bar',
-        'thumbnail' => 'required_if:type,foo|image',
-    ];
+    // ...
 }
 ```
 
-**Result:** ![Form Request](http://marcelpociot.de/documentarian/form_request.png)
+They will be included in the generated documentation text and example requests.
 
-### A note on custom validation rules
-This package only supports custom rules defined as classes. You'll also need to define a `__toString()` method in the class, which should return the description that would be displayed in the generated doc.
+**Result:** ![Form Request](http://marcelpociot.de/documentarian/form_request.png)
 
-#### Controller method doc block
-It is possible to override the results for the response. This will also show the responses for other request methods then GET.
+### 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.
 
-#### @transformer
-With the transformer you can define the transformer that is used for the result of the method. It will try the next parts to get a result if it can find the transformer. The first successfull will be used.
 
-1. Check if there is a transformermodel tag to define the model
-2. Get a model from the modelfactory
-2. If the parameter is a Eloquent model it will load the first from the database.
-3. A new instance from the class
+#### @response
+You can provide an example response for a route by using the `@response` annotation with valid JSON:
 
 ```php
 /**
- * @transformer \Mpociot\ApiDoc\Tests\Fixtures\TestTransformer
+ * @response {
+ *  "id": 4,
+ *  "name": "Jessica Jones",
+ *  "roles": ["admin"]
+ * }
  */
-public function transformerTag()
+public function show($id)
 {
-    return '';
+    return User::find($id);
 }
 ```
 
-#### @transformercollection
-This is the same idea as the @tranformer tag with one different, instead of the return of an item, it will generate the return of a set with two items
+#### @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.
+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`.
+
+Finally, it will pass in the model to the transformer and display the result of that as the example response.
+
+For example:
 
 ```php
 /**
- * @transformercollection \Mpociot\ApiDoc\Tests\Fixtures\TestTransformer
+ * @transformer \App\Transformers\UserTransformer
+ * @transformerModel \App\User
  */
-public function transformerCollectionTag()
+public function listUsers()
 {
-    return '';
+    //...
 }
-```
 
-#### @transformermodel
-The @transformermodel tag is needed for PHP 5.* to get the model. For PHP 7 is it optional to specify the model that is used for the transformer.
-
-#### @response
-If you explicitly want to specify the result of a function you can set it in the docblock as JSON, using the `@response` annotation:
-
-```php
 /**
- * @response {
- *  "token": "eyJ0eXAi…",
- *  "roles": ["admin"]
- * }
+ * @transformer \App\Transformers\UserTransformer
  */
-public function responseTag()
+public function showUser(User $user)
 {
-    return '';
+    //...
 }
-```
-
-#### API responses
-
-If your API route accepts a `GET` method, this package tries to call the API route with all middleware disabled to fetch an example API response. 
-
-If your API needs an authenticated user, you can use the `actAsUserId` option to specify a user ID that will be used for making these API calls:
-
-```sh
-$ php artisan apidoc:generate --routePrefix="api/*" --actAsUserId=1
-```
-
-If you don't want to automatically perform API response calls, use the `noResponseCalls` option.
 
-```sh
-$ php artisan apidoc:generate --routePrefix="api/*" --noResponseCalls
+/**
+ * @transformer \App\Transformers\UserTransformer
+ * @transformerModel \App\User
+ */
+public function showUser(int $id)
+{
+    // ...
+}
 ```
-
-> Note: The example API responses work best with seeded data.
+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
 
-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.
+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, use the `--noPostmanCollection` option, when generating the API documentation.
+If you don't want to create a Postman collection, set the `--postman` config option to false.
 
-As of Laravel 5.3, 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):
+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):
 
 ```php
 'url' => env('APP_URL', 'http://yourappdefault.app'),
@@ -243,16 +275,14 @@ $ php artisan apidoc:update
 
 As an optional parameter, you can use `--location` to tell the update command where your documentation can be found.
 
+If you wish to regenerate your documentation, you can run the `generate` command, you can use the `force` option to force the re-generation of existing/modified API routes.
+
 ## Automatically add markdown to the beginning or end of the documentation
  If you wish to automatically add the same content to the docs every time you generate, you can add a `prepend.md` and/or `append.md` file to the source folder, and they will be included above and below the generated documentation.
  
  **File locations:**
 - `public/docs/source/prepend.md` - Will be added after the front matter and info text
-- `public/docs/source/append.md` - Will be added at the end of the document
-
-## Skip single routes
-
-If you want to skip a single route from a list of routes that match a given prefix, you can use the `@hideFromAPIDocumentation` tag on the Controller method you do not want to document.
+- `public/docs/source/append.md` - Will be added at the end of the document.
 
 ## Further modification
 

+ 3 - 3
composer.json

@@ -18,10 +18,10 @@
         "php": ">=7.0.0",
         "fzaninotto/faker": "~1.8",
         "illuminate/routing": "5.5.* || 5.6.* || 5.7.*",
-        "illuminate/support": "5.5.* 5.6.* || 5.7.*",
-        "illuminate/console": "5.5.* 5.6.* || 5.7.*",
+        "illuminate/support": "5.5.* || 5.6.* || 5.7.*",
+        "illuminate/console": "5.5.* || 5.6.* || 5.7.*",
         "mpociot/documentarian": "^0.2.0",
-        "mpociot/reflection-docblock": "^1.0",
+        "mpociot/reflection-docblock": "^1.0.1",
         "ramsey/uuid": "^3.8"
     },
     "require-dev": {

+ 6 - 9
config/apidoc.php

@@ -77,15 +77,12 @@ return [
              * Specify rules to be applied to all the routes in this group when generating documentation
              */
             'apply' => [
-                'requests' => [
-
-                    /*
-                     * Specify headers to be added to the example requests
-                     */
-                    'headers' => [
-                        // 'Authorization' => 'Bearer: {token}',
-                        // 'Api-Version' => 'v2',
-                    ],
+                /*
+                 * Specify headers to be added to the example requests
+                 */
+                'headers' => [
+                    // 'Authorization' => 'Bearer: {token}',
+                    // 'Api-Version' => 'v2',
                 ],
             ],
         ],

+ 1 - 1
phpunit.xml

@@ -7,7 +7,7 @@
          convertNoticesToExceptions="true"
          convertWarningsToExceptions="true"
          processIsolation="false"
-         stopOnFailure="true">
+         stopOnFailure="false">
     <testsuites>
         <testsuite name="Versionable Suite">
             <directory>tests/</directory>

+ 11 - 2
resources/views/partials/route.blade.php

@@ -11,7 +11,13 @@
 
 ```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['parameters'])) \
+    -H "Accept: application/json"@if(count($parsedRoute['headers'])) \
+@foreach($parsedRoute['headers'] as $header => $value)
+    -H "{{$header}}"="{{$value}}" @if(! ($loop->last))\
+    @endif
+@endforeach
+@endif
+@if(count($parsedRoute['parameters'])) \
 @foreach($parsedRoute['parameters'] as $attribute => $parameter)
     -d "{{$attribute}}"="{{$parameter['value']}}" @if(! ($loop->last))\
     @endif
@@ -30,7 +36,10 @@ var settings = {
 "data": {!! str_replace("\n}","\n    }", str_replace('    ','        ',json_encode(array_combine(array_keys($parsedRoute['parameters']), array_map(function($param){ return $param['value']; },$parsedRoute['parameters'])), JSON_PRETTY_PRINT))) !!},
     @endif
 "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
+@foreach($parsedRoute['headers'] as $header => $value)
+        "{{$header}}": "{{$value}}",
+@endforeach
     }
 }
 

+ 2 - 0
src/ApiDocGeneratorServiceProvider.php

@@ -25,6 +25,8 @@ class ApiDocGeneratorServiceProvider extends ServiceProvider
             __DIR__.'/../config/apidoc.php' => config_path('apidoc.php'),
         ], 'config');
 
+        $this->mergeConfigFrom(__DIR__.'/../config/apidoc.php', 'apidoc');
+
         if ($this->app->runningInConsole()) {
             $this->commands([
                 GenerateDocumentation::class,

+ 19 - 19
src/Commands/GenerateDocumentation.php

@@ -47,14 +47,13 @@ class GenerateDocumentation extends Command
      */
     public function handle()
     {
-        $routes = config('apidoc.router') == 'dingo'
-            ? $this->routeMatcher->getDingoRoutesToBeDocumented(config('apidoc.routes'))
-            : $this->routeMatcher->getLaravelRoutesToBeDocumented(config('apidoc.routes'));
-
-        if ($this->option('router') === 'laravel') {
-            $generator = new LaravelGenerator();
-        } else {
+        $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();
         }
 
         $parsedRoutes = $this->processRoutes($generator, $routes);
@@ -73,7 +72,7 @@ class GenerateDocumentation extends Command
      */
     private function writeMarkdown($parsedRoutes)
     {
-        $outputPath = $this->option('output');
+        $outputPath = config('apidoc.output');
         $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md';
         $compareFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'.compare.md';
         $prependFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'prepend.md';
@@ -81,7 +80,7 @@ class GenerateDocumentation extends Command
 
         $infoText = view('apidoc::partials.info')
             ->with('outputPath', ltrim($outputPath, 'public/'))
-            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'));
+            ->with('showPostmanCollectionButton', config('apidoc.postman'));
 
         $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
             return $routeGroup->map(function ($route) {
@@ -106,15 +105,15 @@ class GenerateDocumentation extends Command
 
             $parsedRouteOutput->transform(function ($routeGroup) use ($generatedDocumentation, $compareDocumentation) {
                 return $routeGroup->transform(function ($route) use ($generatedDocumentation, $compareDocumentation) {
-                    if (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $generatedDocumentation, $routeMatch)) {
-                        $routeDocumentationChanged = (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $compareDocumentation, $compareMatch) && $compareMatch[1] !== $routeMatch[1]);
+                    if (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $generatedDocumentation, $existingRouteDoc)) {
+                        $routeDocumentationChanged = (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]);
                         if ($routeDocumentationChanged === false || $this->option('force')) {
                             if ($routeDocumentationChanged) {
                                 $this->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']);
                             }
                         } else {
                             $this->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']);
-                            $route['modified_output'] = $routeMatch[0];
+                            $route['modified_output'] = $existingRouteDoc[0];
                         }
                     }
 
@@ -136,8 +135,8 @@ class GenerateDocumentation extends Command
             ->with('infoText', $infoText)
             ->with('prependMd', $prependFileContents)
             ->with('appendMd', $appendFileContents)
-            ->with('outputPath', $this->option('output'))
-            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
+            ->with('outputPath', config('apidoc.output'))
+            ->with('showPostmanCollectionButton', config('apidoc.postman'))
             ->with('parsedRoutes', $parsedRouteOutput);
 
         if (! is_dir($outputPath)) {
@@ -154,8 +153,8 @@ class GenerateDocumentation extends Command
             ->with('infoText', $infoText)
             ->with('prependMd', $prependFileContents)
             ->with('appendMd', $appendFileContents)
-            ->with('outputPath', $this->option('output'))
-            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
+            ->with('outputPath', config('apidoc.output'))
+            ->with('showPostmanCollectionButton', config('apidoc.postman'))
             ->with('parsedRoutes', $parsedRouteOutput);
 
         file_put_contents($compareFile, $compareMarkdown);
@@ -168,7 +167,7 @@ class GenerateDocumentation extends Command
 
         $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html');
 
-        if ($this->option('noPostmanCollection') !== true) {
+        if (config('apidoc.postman')) {
             $this->info('Generating Postman collection');
 
             file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
@@ -184,10 +183,11 @@ class GenerateDocumentation extends Command
     private function processRoutes(AbstractGenerator $generator, array $routes)
     {
         $parsedRoutes = [];
-        foreach ($routes as ['route' => $route, 'apply' => $apply]) {
+        foreach ($routes as $routeItem) {
+            $route = $routeItem['route'];
             /** @var Route $route */
             if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
-                $parsedRoutes[] = $generator->processRoute($route, $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));

+ 53 - 36
src/Generators/AbstractGenerator.php

@@ -49,41 +49,23 @@ abstract class AbstractGenerator
      *
      * @return array
      */
-    public function processRoute($route, $apply = [])
+    public function processRoute($route)
     {
         $routeAction = $route->getAction();
         $routeGroup = $this->getRouteGroup($routeAction['uses']);
-        $routeDescription = $this->getRouteDescription($routeAction['uses']);
-        $showresponse = null;
-
-        $response = null;
-        $docblockResponse = $this->getDocblockResponse($routeDescription['tags']);
-        if ($docblockResponse) {
-            // we have a response from the docblock ( @response )
-            $response = $docblockResponse;
-            $showresponse = true;
-        }
-        if (! $response) {
-            $transformerResponse = $this->getTransformerResponse($routeDescription['tags']);
-            if ($transformerResponse) {
-                // we have a transformer response from the docblock ( @transformer || @transformercollection )
-                $response = $transformerResponse;
-                $showresponse = true;
-            }
-        }
-
-        $content = $this->getResponseContent($response);
+        $docBlock = $this->parseDocBlock($routeAction['uses']);
+        $content = $this->getResponse($docBlock['tags']);
 
         return [
             'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
             'resource' => $routeGroup,
-            'title' => $routeDescription['short'],
-            'description' => $routeDescription['long'],
+            'title' => $docBlock['short'],
+            'description' => $docBlock['long'],
             'methods' => $this->getMethods($route),
             'uri' => $this->getUri($route),
-            'parameters' => $this->getParametersFromDocBlock($routeAction['uses']),
+            'parameters' => $this->getParametersFromDocBlock($docBlock['tags']),
             'response' => $content,
-            'showresponse' => $showresponse,
+            'showresponse' => ! empty($content),
         ];
     }
 
@@ -106,11 +88,7 @@ abstract class AbstractGenerator
     protected function getDocblockResponse($tags)
     {
         $responseTags = array_filter($tags, function ($tag) {
-            if (! ($tag instanceof Tag)) {
-                return false;
-            }
-
-            return \strtolower($tag->getName()) == 'response';
+            return $tag instanceof Tag && \strtolower($tag->getName()) == 'response';
         });
         if (empty($responseTags)) {
             return;
@@ -121,13 +99,26 @@ abstract class AbstractGenerator
     }
 
     /**
-     * @param array $routeAction
+     * @param array $tags
      *
      * @return array
      */
-    protected function getParametersFromDocBlock($routeAction)
+    protected function getParametersFromDocBlock($tags)
     {
-        return [];
+        $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;
     }
 
     /**
@@ -180,7 +171,7 @@ abstract class AbstractGenerator
      *
      * @return array
      */
-    protected function getRouteDescription($route)
+    protected function parseDocBlock($route)
     {
         list($class, $method) = explode('@', $route);
         $reflection = new ReflectionClass($class);
@@ -321,8 +312,7 @@ abstract class AbstractGenerator
             if ($modelTag) {
                 $type = $modelTag->getContent();
             }
-            if (version_compare(PHP_VERSION, '7.0.0') >= 0 && \is_null($type)) {
-                // we can only get the type with reflection for PHP 7
+            if (\is_null($type)) {
                 if ($parameter->hasType() &&
                     ! $parameter->getType()->isBuiltin() &&
                     \class_exists((string) $parameter->getType())) {
@@ -369,4 +359,31 @@ abstract class AbstractGenerator
             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 - 152
tests/ApiDocGeneratorTest.php

@@ -1,152 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests;
-
-use Illuminate\Routing\Route;
-use Orchestra\Testbench\TestCase;
-use Mpociot\ApiDoc\Generators\LaravelGenerator;
-use Mpociot\ApiDoc\Tests\Fixtures\TestController;
-use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
-use Illuminate\Support\Facades\Route as RouteFacade;
-
-class ApiDocGeneratorTest extends TestCase
-{
-    /**
-     * @var \Mpociot\ApiDoc\AbstractGenerator
-     */
-    protected $generator;
-
-    protected function getPackageProviders($app)
-    {
-        return [
-            ApiDocGeneratorServiceProvider::class,
-        ];
-    }
-
-    /**
-     * Setup the test environment.
-     */
-    public function setUp()
-    {
-        parent::setUp();
-
-        $this->generator = new LaravelGenerator();
-    }
-
-    public function testCanParseMethodDescription()
-    {
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
-        $route = new Route(['GET'], '/api/test', ['uses' => TestController::class.'@parseMethodDescription']);
-        $parsed = $this->generator->processRoute($route);
-
-        $this->assertSame('Example title.', $parsed['title']);
-        $this->assertSame("This will be the long description.\nIt can also be multiple lines long.", $parsed['description']);
-    }
-
-    public function testCanParseRouteMethods()
-    {
-        RouteFacade::get('/get', TestController::class.'@dummy');
-        RouteFacade::post('/post', TestController::class.'@dummy');
-        RouteFacade::put('/put', TestController::class.'@dummy');
-        RouteFacade::delete('/delete', TestController::class.'@dummy');
-
-        $route = new Route(['GET'], '/get', ['uses' => TestController::class.'@parseMethodDescription']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET'], $parsed['methods']);
-
-        $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@parseMethodDescription']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['POST'], $parsed['methods']);
-
-        $route = new Route(['PUT'], '/put', ['uses' => TestController::class.'@parseMethodDescription']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['PUT'], $parsed['methods']);
-
-        $route = new Route(['DELETE'], '/delete', ['uses' => TestController::class.'@parseMethodDescription']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['DELETE'], $parsed['methods']);
-    }
-
-    public function testCanParseDependencyInjectionInControllerMethods()
-    {
-        RouteFacade::post('/post', TestController::class.'@dependencyInjection');
-        $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@dependencyInjection']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-    }
-
-    public function testCanParseResponseTag()
-    {
-        RouteFacade::post('/responseTag', TestController::class.'@responseTag');
-        $route = new Route(['POST'], '/responseTag', ['uses' => TestController::class.'@responseTag']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-        $this->assertArrayHasKey('showresponse', $parsed);
-        $this->assertTrue($parsed['showresponse']);
-        $this->assertJsonStringEqualsJsonString($parsed['response'], '{ "data": []}');
-    }
-
-    public function testCanParseTransformerTag()
-    {
-        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
-            $this->markTestSkipped('The transformer tag without model need PHP 7');
-        }
-        RouteFacade::post('/transformerTag', TestController::class.'@transformerTag');
-        $route = new Route(['GET'], '/transformerTag', ['uses' => TestController::class.'@transformerTag']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-        $this->assertArrayHasKey('showresponse', $parsed);
-        $this->assertTrue($parsed['showresponse']);
-        $this->assertSame(
-            $parsed['response'],
-            '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}'
-        );
-    }
-
-    public function testCanParseTransformerTagWithModel()
-    {
-        RouteFacade::post('/transformerTagWithModel', TestController::class.'@transformerTagWithModel');
-        $route = new Route(['GET'], '/transformerTagWithModel', ['uses' => TestController::class.'@transformerTagWithModel']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-        $this->assertArrayHasKey('showresponse', $parsed);
-        $this->assertTrue($parsed['showresponse']);
-        $this->assertSame(
-            $parsed['response'],
-            '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}'
-        );
-    }
-
-    public function testCanParseTransformerCollectionTag()
-    {
-        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
-            $this->markTestSkipped('The transformer tag without model need PHP 7');
-        }
-        RouteFacade::post('/transformerCollectionTag', TestController::class.'@transformerCollectionTag');
-        $route = new Route(['GET'], '/transformerCollectionTag', ['uses' => TestController::class.'@transformerCollectionTag']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-        $this->assertArrayHasKey('showresponse', $parsed);
-        $this->assertTrue($parsed['showresponse']);
-        $this->assertSame(
-            $parsed['response'],
-            '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'.
-            '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}'
-        );
-    }
-
-    public function testCanParseTransformerCollectionTagWithModel()
-    {
-        RouteFacade::post('/transformerCollectionTagWithModel', TestController::class.'@transformerCollectionTagWithModel');
-        $route = new Route(['GET'], '/transformerCollectionTagWithModel', ['uses' => TestController::class.'@transformerCollectionTagWithModel']);
-        $parsed = $this->generator->processRoute($route);
-        $this->assertTrue(is_array($parsed));
-        $this->assertArrayHasKey('showresponse', $parsed);
-        $this->assertTrue($parsed['showresponse']);
-        $this->assertSame(
-            $parsed['response'],
-            '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'.
-            '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}'
-        );
-    }
-}

+ 0 - 75
tests/DingoGeneratorTest.php

@@ -1,75 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests;
-
-use Orchestra\Testbench\TestCase;
-use Mpociot\ApiDoc\Generators\DingoGenerator;
-use Dingo\Api\Provider\LaravelServiceProvider;
-use Mpociot\ApiDoc\Tests\Fixtures\TestController;
-use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
-
-class DingoGeneratorTest extends TestCase
-{
-    /**
-     * @var \Mpociot\ApiDoc\Generators\DingoGenerator
-     */
-    protected $generator;
-
-    protected function getPackageProviders($app)
-    {
-        return [
-            LaravelServiceProvider::class,
-            ApiDocGeneratorServiceProvider::class,
-        ];
-    }
-
-    /**
-     * Setup the test environment.
-     */
-    public function setUp()
-    {
-        parent::setUp();
-
-        $this->generator = new DingoGenerator();
-    }
-
-    public function testCanParseMethodDescription()
-    {
-        $api = app('Dingo\Api\Routing\Router');
-        $api->version('v1', function ($api) {
-            $api->get('/api/test', TestController::class.'@parseMethodDescription');
-        });
-        $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[0];
-
-        $parsed = $this->generator->processRoute($route);
-
-        $this->assertSame('Example title.', $parsed['title']);
-        $this->assertSame("This will be the long description.\nIt can also be multiple lines long.", $parsed['description']);
-    }
-
-    public function testCanParseRouteMethods()
-    {
-        $api = app('Dingo\Api\Routing\Router');
-        $api->version('v1', function ($api) {
-            $api->get('/get', TestController::class.'@dummy');
-            $api->post('/post', TestController::class.'@dummy');
-            $api->put('/put', TestController::class.'@dummy');
-            $api->delete('/delete', TestController::class.'@dummy');
-        });
-        $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[0];
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET'], $parsed['methods']);
-
-        $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[1];
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['POST'], $parsed['methods']);
-
-        $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[2];
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['PUT'], $parsed['methods']);
-
-        $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[3];
-        $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['DELETE'], $parsed['methods']);
-    }
-}

+ 0 - 30
tests/Fixtures/CustomValidatorRequest.php

@@ -1,30 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Illuminate\Foundation\Http\FormRequest;
-
-class CustomValidatorRequest extends FormRequest
-{
-    /**
-     * Validate the input.
-     *
-     * @param \Illuminate\Validation\Factory $factory
-     *
-     * @return \Illuminate\Validation\Validator
-     */
-    public function validator($factory)
-    {
-        return $factory->make(
-            $this->validationData(), $this->container->call([$this, 'foo']),
-            $this->messages(), $this->attributes()
-        );
-    }
-
-    public function foo()
-    {
-        return [
-            'required' => 'required',
-        ];
-    }
-}

+ 0 - 18
tests/Fixtures/DependencyInjection.php

@@ -1,18 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Illuminate\Contracts\Filesystem\Filesystem;
-
-class DependencyInjection
-{
-    /**
-     * @var
-     */
-    private $filesystem;
-
-    public function __construct(Filesystem $filesystem)
-    {
-        $this->filesystem = $filesystem;
-    }
-}

+ 0 - 28
tests/Fixtures/DingoTestController.php

@@ -1,28 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Illuminate\Routing\Controller;
-
-class DingoTestController extends Controller
-{
-    public function dummy()
-    {
-        return '';
-    }
-
-    /**
-     * Example title.
-     * This will be the long description.
-     * It can also be multiple lines long.
-     */
-    public function parseMethodDescription()
-    {
-        return '';
-    }
-
-    public function parseFormRequestRules(DingoTestRequest $request)
-    {
-        return '';
-    }
-}

+ 0 - 56
tests/Fixtures/DingoTestRequest.php

@@ -1,56 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Dingo\Api\Http\FormRequest;
-
-class DingoTestRequest extends FormRequest
-{
-    public function rules()
-    {
-        return [
-            'required' => 'required',
-            'accepted' => 'accepted',
-            'after' => 'after:2016-04-23 14:31:00',
-            'active_url' => 'active_url',
-            'alpha' => 'alpha',
-            'alpha_dash' => 'alpha_dash',
-            'alpha_num' => 'alpha_num',
-            'array' => 'array',
-            'before' => 'before:2016-04-23 14:31:00',
-            'between' => 'between:5,200',
-            'string_between' => 'string|between:5,200',
-            'boolean' => 'boolean',
-            'date' => 'date',
-            'date_format' => 'date_format:j.n.Y H:iP',
-            'different' => 'different:alpha_num',
-            'digits' => 'digits:2',
-            'digits_between' => 'digits_between:2,10',
-            'exists' => 'exists:users,firstname',
-            'single_exists' => 'exists:users',
-            'file' => 'file',
-            'in' => 'in:jpeg,png,bmp,gif,svg',
-            'integer' => 'integer',
-            'image' => 'image',
-            'ip' => 'ip',
-            'json' => 'json',
-            'min' => 'min:20',
-            'max' => 'max:10',
-            'mimes' => 'mimes:jpeg,bmp,png',
-            'not_in' => 'not_in:foo,bar',
-            'numeric' => 'numeric',
-            'regex' => 'regex:(.*)',
-            'required_if' => 'required_if:foo,bar',
-            'multiple_required_if' => 'required_if:foo,bar,baz,qux',
-            'required_unless' => 'required_unless:foo,bar',
-            'required_with' => 'required_with:foo,bar,baz',
-            'required_with_all' => 'required_with_all:foo,bar,baz',
-            'required_without' => 'required_without:foo,bar,baz',
-            'required_without_all' => 'required_without_all:foo,bar,baz',
-            'same' => 'same:foo',
-            'size' => 'size:51',
-            'timezone' => 'timezone',
-            'url' => 'url',
-        ];
-    }
-}

+ 0 - 15
tests/Fixtures/DynamicRequest.php

@@ -1,15 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Illuminate\Foundation\Http\FormRequest;
-
-class DynamicRequest extends FormRequest
-{
-    public function rules()
-    {
-        return [
-            'not_in' => 'not_in:'.$this->foo,
-        ];
-    }
-}

+ 20 - 22
tests/Fixtures/TestController.php

@@ -17,22 +17,16 @@ class TestController extends Controller
      * This will be the long description.
      * It can also be multiple lines long.
      */
-    public function parseMethodDescription()
+    public function withEndpointDescription()
     {
         return '';
     }
 
-    public function parseFormRequestRules(TestRequest $request)
-    {
-        return '';
-    }
-
-    public function customFormRequestValidator(CustomValidatorRequest $request)
-    {
-        return '';
-    }
-
-    public function addRouteBindingsToRequestClass(DynamicRequest $request)
+    /**
+     * @bodyParam user_id int required The id of the user.
+     * @bodyParam room_id string The id of the room.
+     */
+    public function withBodyParameters()
     {
         return '';
     }
@@ -42,7 +36,7 @@ class TestController extends Controller
         return $request->headers->all();
     }
 
-    public function fetchRouteResponse()
+    public function shouldFetchRouteResponse()
     {
         $fixture = new \stdClass();
         $fixture->id = 1;
@@ -60,12 +54,12 @@ class TestController extends Controller
         ];
     }
 
-    public function dependencyInjection(DependencyInjection $dependency, TestRequest $request)
-    {
-        return '';
-    }
-
-    public function utf8()
+    /**
+     * @response {
+     *   "result": "Лорем ипсум долор сит амет"
+     * }
+     */
+    public function withUtf8ResponseTag()
     {
         return ['result' => 'Лорем ипсум долор сит амет'];
     }
@@ -79,10 +73,14 @@ class TestController extends Controller
 
     /**
      * @response {
-     *  "data": []
-     *}
+     *   "id": 4,
+     *   "name": "banana",
+     *   "color": "red",
+     *   "weight": "1 kg",
+     *   "delicious": true
+     * }
      */
-    public function responseTag()
+    public function withResponseTag()
     {
         return '';
     }

+ 0 - 56
tests/Fixtures/TestRequest.php

@@ -1,56 +0,0 @@
-<?php
-
-namespace Mpociot\ApiDoc\Tests\Fixtures;
-
-use Illuminate\Foundation\Http\FormRequest;
-
-class TestRequest extends FormRequest
-{
-    public function rules()
-    {
-        return [
-            'required' => 'required',
-            'accepted' => 'accepted',
-            'after' => 'after:2016-04-23 14:31:00',
-            'active_url' => 'active_url',
-            'alpha' => 'alpha',
-            'alpha_dash' => 'alpha_dash',
-            'alpha_num' => 'alpha_num',
-            'array' => 'array',
-            'before' => 'before:2016-04-23 14:31:00',
-            'between' => 'between:5,200',
-            'string_between' => 'string|between:5,200',
-            'boolean' => 'boolean',
-            'date' => 'date',
-            'date_format' => 'date_format:j.n.Y H:iP',
-            'different' => 'different:alpha_num',
-            'digits' => 'digits:2',
-            'digits_between' => 'digits_between:2,10',
-            'exists' => 'exists:users,firstname',
-            'file' => 'file',
-            'single_exists' => 'exists:users',
-            'in' => 'in:jpeg,png,bmp,gif,svg',
-            'image' => 'image',
-            'integer' => 'integer',
-            'ip' => 'ip',
-            'json' => 'json',
-            'min' => 'min:20',
-            'max' => 'max:10',
-            'mimes' => 'mimes:jpeg,bmp,png',
-            'not_in' => 'not_in:foo,bar',
-            'numeric' => 'numeric',
-            'regex' => 'regex:(.*)',
-            'required_if' => 'required_if:foo,bar',
-            'multiple_required_if' => 'required_if:foo,bar,baz,qux',
-            'required_unless' => 'required_unless:foo,bar',
-            'required_with' => 'required_with:foo,bar,baz',
-            'required_with_all' => 'required_with_all:foo,bar,baz',
-            'required_without' => 'required_without:foo,bar,baz',
-            'required_without_all' => 'required_without_all:foo,bar,baz',
-            'same' => 'same:foo',
-            'size' => 'size:51',
-            'timezone' => 'timezone',
-            'url' => 'url',
-        ];
-    }
-}

+ 16 - 0
tests/Fixtures/TestResourceController.php

@@ -10,6 +10,10 @@ class TestResourceController extends Controller
     /**
      * Display a listing of the resource.
      *
+     * @response {
+     *   "index_resource": true
+     * }
+     *
      * @return \Illuminate\Http\Response
      */
     public function index()
@@ -22,6 +26,10 @@ class TestResourceController extends Controller
     /**
      * Show the form for creating a new resource.
      *
+     * @response {
+     *   "create_resource": true
+     * }
+     *
      * @return \Illuminate\Http\Response
      */
     public function create()
@@ -48,6 +56,10 @@ class TestResourceController extends Controller
     /**
      * Display the specified resource.
      *
+     * @response {
+     *   "show_resource": true
+     * }
+     *
      * @param  int  $id
      *
      * @return \Illuminate\Http\Response
@@ -62,6 +74,10 @@ class TestResourceController extends Controller
     /**
      * Show the form for editing the specified resource.
      *
+     * @response {
+     *   "edit_resource": true
+     * }
+     *
      * @param  int  $id
      *
      * @return \Illuminate\Http\Response

+ 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\/fetch","request":{"url":"http:\/\/localhost\/api\/fetch","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":"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":[]}}]}]}

+ 12 - 12
tests/Fixtures/index.md

@@ -41,7 +41,7 @@ var settings = {
     "url": "http://localhost/api/test",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -62,13 +62,13 @@ null
 
 <!-- END_0bef4e738c9d6720ad43b062015d1078 -->
 
-<!-- START_960a1b2b0f0f4dde8ce993307397f9c4 -->
-## api/fetch
+<!-- START_39a6bfda1d6a0c4a5447f51b62557456 -->
+## api/responseTag
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/fetch" \
+curl -X GET -G "http://localhost/api/responseTag" \
     -H "Accept: application/json"
 ```
 
@@ -76,10 +76,10 @@ curl -X GET -G "http://localhost/api/fetch" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/fetch",
+    "url": "http://localhost/api/responseTag",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -92,18 +92,18 @@ $.ajax(settings).done(function (response) {
 
 ```json
 {
-    "id": 1,
-    "name": "Banana",
-    "color": "Red",
-    "weight": "300 grams",
+    "id": 4,
+    "name": "banana",
+    "color": "red",
+    "weight": "1 kg",
     "delicious": true
 }
 ```
 
 ### HTTP Request
-`GET api/fetch`
+`GET api/responseTag`
 
 
-<!-- END_960a1b2b0f0f4dde8ce993307397f9c4 -->
+<!-- END_39a6bfda1d6a0c4a5447f51b62557456 -->
 
 

+ 12 - 12
tests/Fixtures/partial_resource_index.md

@@ -21,13 +21,13 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_2b6e5a4b188cb183c7e59558cce36cb6 -->
+<!-- START_fc1e4f6a697e3c48257de845299b71d5 -->
 ## Display a listing of the resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user" \
+curl -X GET -G "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
@@ -35,10 +35,10 @@ curl -X GET -G "http://localhost/api/user" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user",
+    "url": "http://localhost/api/users",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -56,18 +56,18 @@ $.ajax(settings).done(function (response) {
 ```
 
 ### HTTP Request
-`GET api/user`
+`GET api/users`
 
 
-<!-- END_2b6e5a4b188cb183c7e59558cce36cb6 -->
+<!-- END_fc1e4f6a697e3c48257de845299b71d5 -->
 
-<!-- START_7f66c974d24032cb19061d55d801f62b -->
+<!-- START_5dac10bb34c7618b018b0230d4a51648 -->
 ## Show the form for creating a new resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user/create" \
+curl -X GET -G "http://localhost/api/users/create" \
     -H "Accept: application/json"
 ```
 
@@ -75,10 +75,10 @@ curl -X GET -G "http://localhost/api/user/create" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/create",
+    "url": "http://localhost/api/users/create",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -96,9 +96,9 @@ $.ajax(settings).done(function (response) {
 ```
 
 ### HTTP Request
-`GET api/user/create`
+`GET api/users/create`
 
 
-<!-- END_7f66c974d24032cb19061d55d801f62b -->
+<!-- END_5dac10bb34c7618b018b0230d4a51648 -->
 
 

+ 45 - 45
tests/Fixtures/resource_index.md

@@ -21,13 +21,13 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_2b6e5a4b188cb183c7e59558cce36cb6 -->
+<!-- START_fc1e4f6a697e3c48257de845299b71d5 -->
 ## Display a listing of the resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user" \
+curl -X GET -G "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
@@ -35,10 +35,10 @@ curl -X GET -G "http://localhost/api/user" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user",
+    "url": "http://localhost/api/users",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -56,18 +56,18 @@ $.ajax(settings).done(function (response) {
 ```
 
 ### HTTP Request
-`GET api/user`
+`GET api/users`
 
 
-<!-- END_2b6e5a4b188cb183c7e59558cce36cb6 -->
+<!-- END_fc1e4f6a697e3c48257de845299b71d5 -->
 
-<!-- START_7f66c974d24032cb19061d55d801f62b -->
+<!-- START_5dac10bb34c7618b018b0230d4a51648 -->
 ## Show the form for creating a new resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user/create" \
+curl -X GET -G "http://localhost/api/users/create" \
     -H "Accept: application/json"
 ```
 
@@ -75,10 +75,10 @@ curl -X GET -G "http://localhost/api/user/create" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/create",
+    "url": "http://localhost/api/users/create",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -96,18 +96,18 @@ $.ajax(settings).done(function (response) {
 ```
 
 ### HTTP Request
-`GET api/user/create`
+`GET api/users/create`
 
 
-<!-- END_7f66c974d24032cb19061d55d801f62b -->
+<!-- END_5dac10bb34c7618b018b0230d4a51648 -->
 
-<!-- START_f0654d3f2fc63c11f5723f233cc53c83 -->
+<!-- START_12e37982cc5398c7100e59625ebb5514 -->
 ## Store a newly created resource in storage.
 
 > Example request:
 
 ```bash
-curl -X POST "http://localhost/api/user" \
+curl -X POST "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
@@ -115,10 +115,10 @@ curl -X POST "http://localhost/api/user" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user",
+    "url": "http://localhost/api/users",
     "method": "POST",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -129,18 +129,18 @@ $.ajax(settings).done(function (response) {
 
 
 ### HTTP Request
-`POST api/user`
+`POST api/users`
 
 
-<!-- END_f0654d3f2fc63c11f5723f233cc53c83 -->
+<!-- END_12e37982cc5398c7100e59625ebb5514 -->
 
-<!-- START_ceec0e0b1d13d731ad96603d26bccc2f -->
+<!-- START_8653614346cb0e3d444d164579a0a0a2 -->
 ## Display the specified resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user/{user}" \
+curl -X GET -G "http://localhost/api/users/{user}" \
     -H "Accept: application/json"
 ```
 
@@ -148,10 +148,10 @@ curl -X GET -G "http://localhost/api/user/{user}" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/{user}",
+    "url": "http://localhost/api/users/{user}",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -164,23 +164,23 @@ $.ajax(settings).done(function (response) {
 
 ```json
 {
-    "show_resource": "1"
+    "show_resource": true
 }
 ```
 
 ### HTTP Request
-`GET api/user/{user}`
+`GET api/users/{user}`
 
 
-<!-- END_ceec0e0b1d13d731ad96603d26bccc2f -->
+<!-- END_8653614346cb0e3d444d164579a0a0a2 -->
 
-<!-- START_f4aa12af19ba08e1448d7eafc9f55e67 -->
+<!-- START_11ae28146a4d70ba9a0af9b65d290ad5 -->
 ## Show the form for editing the specified resource.
 
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/user/{user}/edit" \
+curl -X GET -G "http://localhost/api/users/{user}/edit" \
     -H "Accept: application/json"
 ```
 
@@ -188,10 +188,10 @@ curl -X GET -G "http://localhost/api/user/{user}/edit" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/{user}/edit",
+    "url": "http://localhost/api/users/{user}/edit",
     "method": "GET",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -204,23 +204,23 @@ $.ajax(settings).done(function (response) {
 
 ```json
 {
-    "edit_resource": "1"
+    "edit_resource": true
 }
 ```
 
 ### HTTP Request
-`GET api/user/{user}/edit`
+`GET api/users/{user}/edit`
 
 
-<!-- END_f4aa12af19ba08e1448d7eafc9f55e67 -->
+<!-- END_11ae28146a4d70ba9a0af9b65d290ad5 -->
 
-<!-- START_a4a2abed1e8e8cad5e6a3282812fe3f3 -->
+<!-- START_48a3115be98493a3c866eb0e23347262 -->
 ## Update the specified resource in storage.
 
 > Example request:
 
 ```bash
-curl -X PUT "http://localhost/api/user/{user}" \
+curl -X PUT "http://localhost/api/users/{user}" \
     -H "Accept: application/json"
 ```
 
@@ -228,10 +228,10 @@ curl -X PUT "http://localhost/api/user/{user}" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/{user}",
+    "url": "http://localhost/api/users/{user}",
     "method": "PUT",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -242,20 +242,20 @@ $.ajax(settings).done(function (response) {
 
 
 ### HTTP Request
-`PUT api/user/{user}`
+`PUT api/users/{user}`
 
-`PATCH api/user/{user}`
+`PATCH api/users/{user}`
 
 
-<!-- END_a4a2abed1e8e8cad5e6a3282812fe3f3 -->
+<!-- END_48a3115be98493a3c866eb0e23347262 -->
 
-<!-- START_4bb7fb4a7501d3cb1ed21acfc3b205a9 -->
+<!-- START_d2db7a9fe3abd141d5adbc367a88e969 -->
 ## Remove the specified resource from storage.
 
 > Example request:
 
 ```bash
-curl -X DELETE "http://localhost/api/user/{user}" \
+curl -X DELETE "http://localhost/api/users/{user}" \
     -H "Accept: application/json"
 ```
 
@@ -263,10 +263,10 @@ curl -X DELETE "http://localhost/api/user/{user}" \
 var settings = {
     "async": true,
     "crossDomain": true,
-    "url": "http://localhost/api/user/{user}",
+    "url": "http://localhost/api/users/{user}",
     "method": "DELETE",
     "headers": {
-        "accept": "application/json"
+        "accept": "application/json",
     }
 }
 
@@ -277,9 +277,9 @@ $.ajax(settings).done(function (response) {
 
 
 ### HTTP Request
-`DELETE api/user/{user}`
+`DELETE api/users/{user}`
 
 
-<!-- END_4bb7fb4a7501d3cb1ed21acfc3b205a9 -->
+<!-- END_d2db7a9fe3abd141d5adbc367a88e969 -->
 
 

+ 101 - 119
tests/GenerateDocumentationTest.php

@@ -5,36 +5,27 @@ namespace Mpociot\ApiDoc\Tests;
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 use Orchestra\Testbench\TestCase;
+use Illuminate\Support\Facades\App;
 use Illuminate\Contracts\Console\Kernel;
-use Dingo\Api\Provider\LaravelServiceProvider;
-use Mpociot\ApiDoc\Generators\LaravelGenerator;
 use Mpociot\ApiDoc\Tests\Fixtures\TestController;
 use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
 use Illuminate\Support\Facades\Route as RouteFacade;
-use Mpociot\ApiDoc\Tests\Fixtures\DingoTestController;
 use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController;
 
 class GenerateDocumentationTest extends TestCase
 {
-    /**
-     * @var \Mpociot\ApiDoc\AbstractGenerator
-     */
-    protected $generator;
-
     /**
      * Setup the test environment.
      */
     public function setUp()
     {
         parent::setUp();
-
-        $this->generator = new LaravelGenerator();
     }
 
     public function tearDown()
     {
         // delete the generated docs - compatible cross-platform
-        $dir = __DIR__.'/../public/docs'; /*
+        $dir = __DIR__.'/../public/docs';
         if (is_dir($dir)) {
             $files = new RecursiveIteratorIterator(
                 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
@@ -46,7 +37,7 @@ class GenerateDocumentationTest extends TestCase
                 $todo($fileinfo->getRealPath());
             }
             rmdir($dir);
-        }*/
+        }
     }
 
     /**
@@ -57,107 +48,118 @@ class GenerateDocumentationTest extends TestCase
     protected function getPackageProviders($app)
     {
         return [
-            LaravelServiceProvider::class,
+            \Dingo\Api\Provider\LaravelServiceProvider::class,
             ApiDocGeneratorServiceProvider::class,
         ];
     }
 
-    public function testConsoleCommandNeedsPrefixesOrDomainsOrRoutes()
-    {
-        $output = $this->artisan('apidoc:generate');
-        $this->assertEquals('You must provide either a route prefix, a route domain, a route or a middleware to generate the documentation.'.PHP_EOL, $output);
-    }
-
-    public function testConsoleCommandDoesNotWorkWithClosure()
+    /** @test */
+    public function console_command_does_not_work_with_closure()
     {
         RouteFacade::get('/api/closure', function () {
-            return 'foo';
+            return 'hi';
         });
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
+        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
+
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $output = $this->artisan('apidoc:generate');
 
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
         $this->assertContains('Skipping route: [GET] api/closure', $output);
         $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
-    public function testConsoleCommandDoesNotWorkWithClosureUsingDingo()
+    /** @test */
+    public function console_command_does_not_work_with_closure_using_dingo()
     {
-        $api = app('Dingo\Api\Routing\Router');
+        $api = app(\Dingo\Api\Routing\Router::class);
         $api->version('v1', function ($api) {
-            $api->get('v1/closure', function () {
+            $api->get('/closure', function () {
                 return 'foo';
             });
-            $api->get('v1/test', DingoTestController::class.'@parseMethodDescription');
-
-            $output = $this->artisan('apidoc:generate', [
-                '--router' => 'dingo',
-                '--routePrefix' => 'v1/*',
-            ]);
-            $this->assertContains('Skipping route: [GET] closure', $output);
-            $this->assertContains('Processed route: [GET] test', $output);
+            $api->get('/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->assertContains('Skipping route: [GET] closure', $output);
+        $this->assertContains('Processed route: [GET] test', $output);
     }
 
-    public function testCanSkipSingleRoutesCommandDoesNotWorkWithClosure()
+    /** @test */
+    public function can_skip_single_routes()
     {
         RouteFacade::get('/api/skip', TestController::class.'@skip');
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
+        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
+
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $output = $this->artisan('apidoc:generate');
 
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
         $this->assertContains('Skipping route: [GET] api/skip', $output);
         $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
-    public function testCanParseResourceRoutes()
+    /** @test */
+    public function can_parse_resource_routes()
     {
-        RouteFacade::resource('/api/user', TestResourceController::class);
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        RouteFacade::resource('/api/users', TestResourceController::class);
+
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $this->artisan('apidoc:generate');
+
         $fixtureMarkdown = __DIR__.'/Fixtures/resource_index.md';
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
         $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown);
     }
 
-    public function testCanParsePartialResourceRoutes()
+    /** @test */
+    public function can_parse_partial_resource_routes()
     {
-        RouteFacade::resource('/api/user', TestResourceController::class, [
-            'only' => [
-                'index', 'create',
-            ],
-        ]);
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        if (version_compare(App::version(), '5.6', '<')) {
+            RouteFacade::resource('/api/users', TestResourceController::class, [
+                'only' => [
+                    'index', 'create',
+                ],
+            ]);
+        } else {
+            RouteFacade::resource('/api/users', TestResourceController::class)
+                ->only(['index', 'create']);
+        }
+
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $this->artisan('apidoc:generate');
+
         $fixtureMarkdown = __DIR__.'/Fixtures/partial_resource_index.md';
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
         $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown);
 
-        RouteFacade::apiResource('/api/user', TestResourceController::class, [
-            'only' => [
-                'index', 'create',
-            ],
-        ]);
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        if (version_compare(App::version(), '5.6', '<')) {
+            RouteFacade::apiResource('/api/users', TestResourceController::class, [
+                'only' => [
+                    'index', 'create',
+                ],
+            ]);
+        } else {
+            RouteFacade::apiResource('/api/users', TestResourceController::class)
+                ->only(['index', 'create']);
+        }
+        $this->artisan('apidoc:generate');
+
         $fixtureMarkdown = __DIR__.'/Fixtures/partial_resource_index.md';
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
         $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown);
     }
 
-    public function testGeneratedMarkdownFileIsCorrect()
+    /** @test */
+    public function generated_markdown_file_is_correct()
     {
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
-        RouteFacade::get('/api/fetch', TestController::class.'@fetchRouteResponse');
+        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
+        RouteFacade::get('/api/responseTag', TestController::class.'@withResponseTag');
 
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $this->artisan('apidoc:generate');
 
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
         $compareMarkdown = __DIR__.'/../public/docs/source/.compare.md';
@@ -166,87 +168,67 @@ class GenerateDocumentationTest extends TestCase
         $this->assertFilesHaveSameContent($fixtureMarkdown, $compareMarkdown);
     }
 
-    public function testCanPrependAndAppendDataToGeneratedMarkdown()
+    /** @test */
+    public function can_prepend_and_append_data_to_generated_markdown()
     {
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
-        RouteFacade::get('/api/fetch', TestController::class.'@fetchRouteResponse');
+        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
+        RouteFacade::get('/api/responseTag', TestController::class.'@withResponseTag');
 
-        $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        $this->artisan('apidoc:generate');
 
         $prependMarkdown = __DIR__.'/Fixtures/prepend.md';
         $appendMarkdown = __DIR__.'/Fixtures/append.md';
         copy($prependMarkdown, __DIR__.'/../public/docs/source/prepend.md');
         copy($appendMarkdown, __DIR__.'/../public/docs/source/append.md');
 
-        $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        $this->artisan('apidoc:generate');
 
         $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
-        $this->assertContainsRaw($this->getFileContents($prependMarkdown), $this->getFileContents($generatedMarkdown));
-        $this->assertContainsRaw($this->getFileContents($appendMarkdown), $this->getFileContents($generatedMarkdown));
+        $this->assertContainsIgnoringWhitespace($this->getFileContents($prependMarkdown), $this->getFileContents($generatedMarkdown));
+        $this->assertContainsIgnoringWhitespace($this->getFileContents($appendMarkdown), $this->getFileContents($generatedMarkdown));
     }
 
-    public function testAddsBindingsToGetRouteRules()
+    /** @test */
+    public function generated_postman_collection_file_is_correct()
     {
-        RouteFacade::get('/api/test/{foo}', TestController::class.'@addRouteBindingsToRequestClass');
+        RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription');
+        RouteFacade::post('/api/responseTag', TestController::class.'@withResponseTag');
 
-        $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-            '--bindings' => 'foo,bar',
-        ]);
-
-        $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md');
-
-        $this->assertContains('Not in: `bar`', $generatedMarkdown);
-    }
-
-    public function testGeneratedPostmanCollectionFileIsCorrect()
-    {
-        RouteFacade::get('/api/test', TestController::class.'@parseMethodDescription');
-        RouteFacade::post('/api/fetch', TestController::class.'@fetchRouteResponse');
-
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        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'));
         $this->assertEquals($generatedCollection, $fixtureCollection);
     }
 
-    public function testCanAppendCustomHttpHeaders()
+    /** @test */
+    public function can_append_custom_http_headers()
     {
         RouteFacade::get('/api/headers', TestController::class.'@checkCustomHeaders');
 
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-            '--header' => [
-                'Authorization: customAuthToken',
-                'X-Custom-Header: foobar',
+        config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
+        config([
+            'apidoc.routes.0.apply.headers' => [
+                'Authorization' => 'customAuthToken',
+                'Custom-Header' => 'NotSoCustom',
             ],
         ]);
+        $this->artisan('apidoc:generate');
 
         $generatedMarkdown = $this->getFileContents(__DIR__.'/../public/docs/source/index.md');
-        $this->assertContainsRaw('"authorization": [
-        "customAuthToken"
-    ],
-    "x-custom-header": [
-        "foobar"
-    ]', $generatedMarkdown);
+        $this->assertContainsIgnoringWhitespace('"Authorization": "customAuthToken","Custom-Header":"NotSoCustom"', $generatedMarkdown);
     }
 
-    public function testGeneratesUTF8Responses()
+    /** @test */
+    public function can_parse_utf8_response()
     {
-        RouteFacade::get('/api/utf8', TestController::class.'@utf8');
+        RouteFacade::get('/api/utf8', TestController::class.'@withUtf8ResponseTag');
 
-        $output = $this->artisan('apidoc:generate', [
-            '--routePrefix' => 'api/*',
-        ]);
+        config(['apidoc.routes.0.prefixes' => ['api/*']]);
+        $this->artisan('apidoc:generate');
 
         $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md');
         $this->assertContains('Лорем ипсум долор сит амет', $generatedMarkdown);
@@ -290,7 +272,7 @@ class GenerateDocumentationTest extends TestCase
      * @param $needle
      * @param $haystack
      */
-    private function assertContainsRaw($needle, $haystack)
+    private function assertContainsIgnoringWhitespace($needle, $haystack)
     {
         $haystack = preg_replace('/\s/', '', $haystack);
         $needle = preg_replace('/\s/', '', $needle);

+ 38 - 0
tests/Unit/DingoGeneratorTest.php

@@ -0,0 +1,38 @@
+<?php
+
+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;
+
+class DingoGeneratorTest extends GeneratorTestCase
+{
+    protected function getPackageProviders($app)
+    {
+        return [
+            \Dingo\Api\Provider\LaravelServiceProvider::class,
+            ApiDocGeneratorServiceProvider::class,
+        ];
+    }
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->generator = new DingoGenerator();
+    }
+
+    public function createRoute(string $httpMethod, string $path, string $controllerMethod)
+    {
+        $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");
+        });
+
+        return $route;
+    }
+}

+ 161 - 0
tests/Unit/GeneratorTestCase.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tests\Unit;
+
+use Orchestra\Testbench\TestCase;
+use Mpociot\ApiDoc\Generators\LaravelGenerator;
+use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
+
+abstract class GeneratorTestCase extends TestCase
+{
+    /**
+     * @var \Mpociot\ApiDoc\Generators\AbstractGenerator
+     */
+    protected $generator;
+
+    protected function getPackageProviders($app)
+    {
+        return [
+            ApiDocGeneratorServiceProvider::class,
+        ];
+    }
+
+    /**
+     * Setup the test environment.
+     */
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->generator = new LaravelGenerator();
+    }
+
+    /** @test */
+    public function test_can_parse_endpoint_description()
+    {
+        $route = $this->createRoute('GET', '/api/test', 'withEndpointDescription');
+        $parsed = $this->generator->processRoute($route);
+
+        $this->assertSame('Example title.', $parsed['title']);
+        $this->assertSame("This will be the long description.\nIt can also be multiple lines long.", $parsed['description']);
+    }
+
+    /** @test */
+    public function test_can_parse_body_parameters()
+    {
+        $route = $this->createRoute('GET', '/api/test', 'withBodyParameters');
+        $parameters = $this->generator->processRoute($route)['parameters'];
+
+        $this->assertArraySubset([
+            'user_id' => [
+                'type' => 'integer',
+                'required' => true,
+                'description' => 'The id of the user.',
+            ],
+            'room_id' => [
+                'type' => 'string',
+                'required' => false,
+                'description' => 'The id of the room.',
+            ],
+        ], $parameters);
+    }
+
+    /** @test */
+    public function test_can_parse_route_methods()
+    {
+        $route = $this->createRoute('GET', '/get', 'withEndpointDescription');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertSame(['GET'], $parsed['methods']);
+
+        $route = $this->createRoute('POST', '/post', 'withEndpointDescription');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertSame(['POST'], $parsed['methods']);
+
+        $route = $this->createRoute('PUT', '/put', 'withEndpointDescription');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertSame(['PUT'], $parsed['methods']);
+
+        $route = $this->createRoute('DELETE', '/delete', 'withEndpointDescription');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertSame(['DELETE'], $parsed['methods']);
+    }
+
+    /** @test */
+    public function test_can_parse_response_tag()
+    {
+        $route = $this->createRoute('POST', '/responseTag', 'withResponseTag');
+
+        $parsed = $this->generator->processRoute($route);
+
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertJsonStringEqualsJsonString(json_encode([
+            'id' => 4,
+            'name' => 'banana',
+            'color' => 'red',
+            'weight' => '1 kg',
+            'delicious' => true,
+        ]), $parsed['response']);
+    }
+
+    /** @test */
+    public function test_can_parse_transformer_tag()
+    {
+        $route = $this->createRoute('GET', '/transformerTag', 'transformerTag');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertSame(
+            $parsed['response'],
+            '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}'
+        );
+    }
+
+    /** @test */
+    public function test_can_parse_transformer_tag_with_model()
+    {
+        $route = $this->createRoute('GET', '/transformerTagWithModel', 'transformerTagWithModel');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertSame(
+            $parsed['response'],
+            '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}'
+        );
+    }
+
+    /** @test */
+    public function test_can_parse_transformer_collection_tag()
+    {
+        $route = $this->createRoute('GET', '/transformerCollectionTag', 'transformerCollectionTag');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertSame(
+            $parsed['response'],
+            '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'.
+            '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}'
+        );
+    }
+
+    /** @test */
+    public function test_can_parse_transformer_collection_tag_with_model()
+    {
+        $route = $this->createRoute('GET', '/transformerCollectionTagWithModel', 'transformerCollectionTagWithModel');
+        $parsed = $this->generator->processRoute($route);
+        $this->assertTrue(is_array($parsed));
+        $this->assertArrayHasKey('showresponse', $parsed);
+        $this->assertTrue($parsed['showresponse']);
+        $this->assertSame(
+            $parsed['response'],
+            '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'.
+            '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}'
+        );
+    }
+
+    abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod);
+}

+ 30 - 0
tests/Unit/LaravelGeneratorTest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tests\Unit;
+
+use Illuminate\Routing\Route;
+use Mpociot\ApiDoc\Generators\LaravelGenerator;
+use Mpociot\ApiDoc\Tests\Fixtures\TestController;
+use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider;
+
+class LaravelGeneratorTest extends GeneratorTestCase
+{
+    protected function getPackageProviders($app)
+    {
+        return [
+            ApiDocGeneratorServiceProvider::class,
+        ];
+    }
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->generator = new LaravelGenerator();
+    }
+
+    public function createRoute(string $httpMethod, string $path, string $controllerMethod)
+    {
+        return new Route([$httpMethod], $path, ['uses' => TestController::class."@$controllerMethod"]);
+    }
+}

+ 1 - 1
tests/RouteMatcherTest.php → tests/Unit/RouteMatcherTest.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace Mpociot\ApiDoc\Tests;
+namespace Mpociot\ApiDoc\Tests\Unit;
 
 use Dingo\Api\Routing\Router;
 use Orchestra\Testbench\TestCase;