Browse Source

Merge branch 'master' into master

Shalvah A 6 năm trước cách đây
mục cha
commit
789413ce3d

+ 0 - 2
.styleci.yml

@@ -4,5 +4,3 @@ enabled:
   - phpdoc_order
   - phpdoc_separation
   - unalign_double_arrow
-
-linting: true

+ 13 - 5
README.md

@@ -9,7 +9,7 @@ Automatically generate your API documentation from your existing Laravel routes.
 [![codecov.io](https://codecov.io/github/mpociot/laravel-apidoc-generator/coverage.svg?branch=master)](https://codecov.io/github/mpociot/laravel-apidoc-generator?branch=master)
 [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mpociot/laravel-apidoc-generator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mpociot/laravel-apidoc-generator/?branch=master)
 [![Build Status](https://travis-ci.org/mpociot/laravel-apidoc-generator.svg?branch=master)](https://travis-ci.org/mpociot/laravel-apidoc-generator)
-[![StyleCI](https://styleci.io/repos/57999295/shield)](https://styleci.io/repos/57999295)
+[![StyleCI](https://styleci.io/repos/57999295/shield?style=flat)](https://styleci.io/repos/57999295)
 [![Dependency Status](https://www.versioneye.com/php/mpociot:laravel-apidoc-generator/dev-master/badge?style=flat)](https://www.versioneye.com/php/mpociot:laravel-apidoc-generator/dev-master)
 
 
@@ -20,7 +20,7 @@ Require this package with composer using the following command:
 ```sh
 $ composer require mpociot/laravel-apidoc-generator
 ```
-Go to your `config/app.php` and add the service provider:
+Using Laravel < 5.5? Go to your `config/app.php` and add the service provider:
 
 ```php
 Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class,
@@ -34,7 +34,15 @@ To generate your API documentation, use the `api:generate` artisan command.
 
 ```sh
 $ php artisan api:generate --routePrefix="api/v1/*"
+
 ```
+You can pass in multiple prefixes by spearating each prefix with comma.
+
+```sh
+$ php artisan api:generate --routePrefix="api/v1/*,api/public/*"
+```
+It will generate documentation for all of the routes whose prefixes are `api/v1/` and `api/public/`
+
 
 This command will scan your applications routes for the URIs matching `api/v1/*` and will parse these controller methods and form requests. For example:
 
@@ -53,7 +61,7 @@ Route::group(array('prefix' => 'api/v1', 'middleware' => []), function () {
 Option | Description
 --------- | -------
 `output` | The output path used for the generated documentation. Default: `public/docs`
-`routePrefix` | The route prefix to use for generation - `*` can be used as a wildcard
+`routePrefix` | The route prefix to use for generation - `*` can be used as a wildcard 
 `routes` | The route names to use for generation - Required if no routePrefix is provided
 `middleware` | The middlewares to use for generation
 `noResponseCalls` | Disable API response calls
@@ -63,7 +71,7 @@ Option | Description
 `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'`
+`header` | Custom HTTP headers to add to the example requests. Separate the header name and value with ":". For example: `--header="Authorization: CustomToken"`
 
 ## Publish rule descriptions for customisation or translation.
 
@@ -206,7 +214,7 @@ The generator automatically creates a Postman collection file, which you can imp
 
 If you don't want to create a Postman collection, use the `--noPostmanCollection` option, when generating the API documentation.
 
-As of 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):
+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):
 
 ```php
 'url' => env('APP_URL', 'http://yourappdefault.app'),

+ 7 - 0
composer.json

@@ -37,5 +37,12 @@
         "psr-4": {
             "Mpociot\\ApiDoc\\Tests\\": "tests/"
         }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Mpociot\\ApiDoc\\ApiDocGeneratorServiceProvider"
+            ]
+       }
     }
 }

+ 33 - 17
src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php

@@ -22,8 +22,8 @@ class GenerateDocumentation extends Command
      */
     protected $signature = 'api:generate
                             {--output=public/docs : The output path for the generated documentation}
-                            {--routeDomain= : The route domain to use for generation}
-                            {--routePrefix= : The route prefix to use for generation}
+                            {--routeDomain= : The route domain (or domains) to use for generation}
+                            {--routePrefix= : The route prefix (or prefixes) to use for generation}
                             {--routes=* : The route names to use for generation}
                             {--middleware= : The middleware to use for generation}
                             {--noResponseCalls : Disable API response calls}
@@ -76,18 +76,30 @@ class GenerateDocumentation extends Command
         $this->setUserToBeImpersonated($this->option('actAsUserId'));
 
         if ($routePrefix === null && $routeDomain === null && ! count($allowedRoutes) && $middleware === null) {
-            $this->error('You must provide either a route prefix or a route domain a route or a middleware to generate the documentation.');
+            $this->error('You must provide either a route prefix or a route domain or a middleware to generate the documentation.');
 
             return false;
         }
 
         $generator->prepareMiddleware($this->option('useMiddlewares'));
 
+        $routePrefixes = explode(',', $routePrefix ?: '*');
+        $routeDomains = explode(',', $routeDomain ?: '*');
+
+        $parsedRoutes = [];
+
         if ($this->option('router') === 'laravel') {
-            $parsedRoutes = $this->processLaravelRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+            foreach ($routeDomains as $routeDomain) {
+                foreach ($routePrefixes as $routePrefix) {
+                    $parsedRoutes += $this->processLaravelRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+                }
+            }
         } else {
-            // TODO: implement Dingo domain route filter
-            $parsedRoutes = $this->processDingoRoutes($generator, $allowedRoutes, $routePrefix, $middleware);
+            foreach ($routeDomains as $routeDomain) {
+                foreach ($routePrefixes as $routePrefix) {
+                    $parsedRoutes += $this->processDingoRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+                }
+            }
         }
         $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sort(function ($a, $b) {
             return strcmp($a->first()['resource'], $b->first()['resource']);
@@ -128,10 +140,6 @@ class GenerateDocumentation extends Command
             $generatedDocumentation = file_get_contents($targetFile);
             $compareDocumentation = file_get_contents($compareFile);
 
-            if (preg_match('/<!-- START_INFO -->(.*)<!-- END_INFO -->/is', $generatedDocumentation, $generatedInfoText)) {
-                $infoText = trim($generatedInfoText[1], "\n");
-            }
-
             if (preg_match('/---(.*)---\\s<!-- START_INFO -->/is', $generatedDocumentation, $generatedFrontmatter)) {
                 $frontmatter = trim($generatedFrontmatter[1], "\n");
             }
@@ -225,12 +233,12 @@ class GenerateDocumentation extends Command
         if (! empty($actAs)) {
             if (version_compare($this->laravel->version(), '5.2.0', '<')) {
                 $userModel = config('auth.model');
-                $user = $userModel::find((int) $actAs);
+                $user = $userModel::find($actAs);
                 $this->laravel['auth']->setUser($user);
             } else {
                 $provider = $this->option('authProvider');
                 $userModel = config("auth.providers.$provider.model");
-                $user = $userModel::find((int) $actAs);
+                $user = $userModel::find($actAs);
                 $this->laravel['auth']->guard($this->option('authGuard'))->setUser($user);
             }
         }
@@ -264,9 +272,10 @@ class GenerateDocumentation extends Command
         $parsedRoutes = [];
         foreach ($routes as $route) {
             if (in_array($route->getName(), $allowedRoutes)
-                || str_is($routeDomain, $generator->getDomain($route))
-                || str_is($routePrefix, $generator->getUri($route))
-                || in_array($middleware, $route->middleware())) {
+                || (str_is($routeDomain, $generator->getDomain($route)) 
+                    && str_is($routePrefix, $generator->getUri($route)))
+                || in_array($middleware, $route->middleware())
+               ) {
                 if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
                     $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse);
                     $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
@@ -282,18 +291,25 @@ class GenerateDocumentation extends Command
     /**
      * @param AbstractGenerator $generator
      * @param $allowedRoutes
+     * @param $routeDomain
      * @param $routePrefix
      *
      * @return array
      */
-    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix, $middleware)
+    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware)
     {
         $withResponse = $this->option('noResponseCalls') === false;
         $routes = $this->getRoutes();
         $bindings = $this->getBindings();
         $parsedRoutes = [];
         foreach ($routes as $route) {
-            if (empty($allowedRoutes) || in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->uri()) || in_array($middleware, $route->middleware())) {
+            if (empty($allowedRoutes) 
+                // TODO extract this into a method
+                || in_array($route->getName(), $allowedRoutes)
+                || (str_is($routeDomain, $generator->getDomain($route)) 
+                    && str_is($routePrefix, $generator->getUri($route))) 
+                || in_array($middleware, $route->middleware())
+               ) {
                 if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
                     $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse);
                     $this->info('Processed route: ['.implode(',', $route->getMethods()).'] '.$route->uri());

+ 43 - 7
src/Mpociot/ApiDoc/Generators/AbstractGenerator.php

@@ -51,7 +51,7 @@ abstract class AbstractGenerator
      *
      * @return  void
      */
-    abstract public function prepareMiddleware($disable = false);
+    abstract public function prepareMiddleware($enable = false);
 
     /**
      * Get the response from the docblock if available.
@@ -74,7 +74,7 @@ abstract class AbstractGenerator
         }
         $responseTag = \array_first($responseTags);
 
-        return \response(\json_encode($responseTag->getContent()));
+        return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']);
     }
 
     /**
@@ -86,8 +86,9 @@ abstract class AbstractGenerator
      */
     protected function getParameters($routeData, $routeAction, $bindings)
     {
-        $validator = Validator::make([], $this->getRouteRules($routeAction['uses'], $bindings));
-        foreach ($validator->getRules() as $attribute => $rules) {
+        $rules = $this->simplifyRules($this->getRouteRules($routeAction['uses'], $bindings));
+
+        foreach ($rules as $attribute => $rules) {
             $attributeData = [
                 'required' => false,
                 'type' => null,
@@ -95,6 +96,7 @@ abstract class AbstractGenerator
                 'value' => '',
                 'description' => [],
             ];
+
             foreach ($rules as $ruleName => $rule) {
                 $this->parseRule($rule, $attribute, $attributeData, $routeData['id']);
             }
@@ -104,6 +106,31 @@ abstract class AbstractGenerator
         return $routeData;
     }
 
+    /**
+     * Format the validation rules as plain array.
+     *
+     * @param array $rules
+     *
+     * @return array
+     */
+    protected function simplifyRules($rules)
+    {
+        $simplifiedRules = Validator::make([], $rules)->getRules();
+
+        if (count($simplifiedRules) === 0) {
+            return $simplifiedRules;
+        }
+
+        $values = collect($simplifiedRules)
+            ->filter(function ($values) {
+                return in_array('array', $values);
+            })->map(function ($val, $key) {
+                return [''];
+            })->all();
+
+        return Validator::make($values, $rules)->getRules();
+    }
+
     /**
      * @param  $route
      * @param  $bindings
@@ -119,13 +146,15 @@ abstract class AbstractGenerator
 
         // Split headers into key - value pairs
         $headers = collect($headers)->map(function ($value) {
-            $split = explode(':', $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($split[0]) => trim($split[1])];
+            return [trim($key) => trim($value)];
         })->collapse()->toArray();
 
         //Changes url with parameters like /users/{user} to /users/1
-        $uri = preg_replace('/{(.*?)}/', 1, $uri);
+        $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters
 
         return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
     }
@@ -141,6 +170,7 @@ abstract class AbstractGenerator
         $uri = $this->getUri($route);
         foreach ($bindings as $model => $id) {
             $uri = str_replace('{'.$model.'}', $id, $uri);
+            $uri = str_replace('{'.$model.'?}', $id, $uri);
         }
 
         return $uri;
@@ -440,6 +470,12 @@ abstract class AbstractGenerator
                 $attributeData['value'] = $faker->ipv4;
                 $attributeData['type'] = $rule;
                 break;
+            default:
+                $unknownRuleDescription = Description::parse($rule)->getDescription();
+                if ($unknownRuleDescription) {
+                    $attributeData['description'][] = $unknownRuleDescription;
+                }
+                break;
         }
 
         if ($attributeData['value'] === '') {

+ 1 - 1
src/Mpociot/ApiDoc/Generators/DingoGenerator.php

@@ -89,6 +89,6 @@ class DingoGenerator extends AbstractGenerator
      */
     public function getMethods($route)
     {
-        return $route->getMethods();
+        return array_diff($route->getMethods(), ['HEAD']);
     }
 }

+ 7 - 10
src/Mpociot/ApiDoc/Generators/LaravelGenerator.php

@@ -49,10 +49,12 @@ class LaravelGenerator extends AbstractGenerator
     public function getMethods($route)
     {
         if (version_compare(app()->version(), '5.4', '<')) {
-            return $route->getMethods();
+            $methods = $route->getMethods();
+        } else {
+            $methods = $route->methods();
         }
 
-        return $route->methods();
+        return array_diff($methods, ['HEAD']);
     }
 
     /**
@@ -97,7 +99,7 @@ class LaravelGenerator extends AbstractGenerator
                 $response = $this->getRouteResponse($route, $bindings, $headers);
             }
             if ($response->headers->get('Content-Type') === 'application/json') {
-                $content = json_encode(json_decode($response->getContent()), JSON_PRETTY_PRINT);
+                $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
             } else {
                 $content = $response->getContent();
             }
@@ -123,9 +125,9 @@ class LaravelGenerator extends AbstractGenerator
      *
      * @return  void
      */
-    public function prepareMiddleware($disable = true)
+    public function prepareMiddleware($enable = true)
     {
-        App::instance('middleware.disable', true);
+        App::instance('middleware.disable', ! $enable);
     }
 
     /**
@@ -158,11 +160,6 @@ class LaravelGenerator extends AbstractGenerator
 
         $kernel->terminate($request, $response);
 
-        if (file_exists($file = App::bootstrapPath().'/app.php')) {
-            $app = require $file;
-            $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
-        }
-
         return $response;
     }
 

+ 1 - 1
src/resources/views/documentarian.blade.php

@@ -7,7 +7,7 @@
 
 @foreach($parsedRoutes as $group => $routes)
 @if($group)
-#{{$group}}
+#{!! $group !!}
 @endif
 @foreach($routes as $parsedRoute)
 @if($writeCompareFile === true)

+ 1 - 1
src/resources/views/partials/route.blade.php

@@ -38,7 +38,7 @@ $.ajax(settings).done(function (response) {
 });
 ```
 
-@if(in_array('GET',$parsedRoute['methods']) || isset($parsedRoute['showresponse']) && $parsedRoute['showresponse'])
+@if(in_array('GET',$parsedRoute['methods']) || (isset($parsedRoute['showresponse']) && $parsedRoute['showresponse']))
 > Example response:
 
 ```json

+ 2 - 2
tests/ApiDocGeneratorTest.php

@@ -53,7 +53,7 @@ class ApiDocGeneratorTest extends TestCase
 
         $route = new Route(['GET'], '/get', ['uses' => TestController::class.'@parseMethodDescription']);
         $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET', 'HEAD'], $parsed['methods']);
+        $this->assertSame(['GET'], $parsed['methods']);
 
         $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@parseMethodDescription']);
         $parsed = $this->generator->processRoute($route);
@@ -345,7 +345,7 @@ class ApiDocGeneratorTest extends TestCase
         $this->assertTrue(is_array($parsed));
         $this->assertArrayHasKey('showresponse', $parsed);
         $this->assertTrue($parsed['showresponse']);
-        $this->assertSame($parsed['response'], '"{\n data: [],\n}"');
+        $this->assertSame($parsed['response'], "{\n data: [],\n}");
     }
 
     public function testCanParseTransformerTag()

+ 1 - 1
tests/DingoGeneratorTest.php

@@ -68,7 +68,7 @@ class DingoGeneratorTest extends TestCase
         });
         $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[0];
         $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET', 'HEAD'], $parsed['methods']);
+        $this->assertSame(['GET'], $parsed['methods']);
 
         $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[1];
         $parsed = $this->generator->processRoute($route);

+ 4 - 8
tests/Fixtures/index.md

@@ -21,7 +21,7 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_08307893aff90cc5097c48a1c8fc2f6d -->
+<!-- START_0bef4e738c9d6720ad43b062015d1078 -->
 ## Example title.
 
 This will be the long description.
@@ -59,12 +59,10 @@ null
 ### HTTP Request
 `GET api/test`
 
-`HEAD api/test`
 
+<!-- END_0bef4e738c9d6720ad43b062015d1078 -->
 
-<!-- END_08307893aff90cc5097c48a1c8fc2f6d -->
-
-<!-- START_8ba174f2507a0967efd46fab3764b80e -->
+<!-- START_960a1b2b0f0f4dde8ce993307397f9c4 -->
 ## api/fetch
 
 > Example request:
@@ -105,8 +103,6 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/fetch`
 
-`HEAD api/fetch`
-
 
-<!-- END_8ba174f2507a0967efd46fab3764b80e -->
+<!-- END_960a1b2b0f0f4dde8ce993307397f9c4 -->
 

+ 8 - 16
tests/Fixtures/resource_index.md

@@ -21,7 +21,7 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_2ea88ff35aa222f5582e50f39a2b35fd -->
+<!-- START_2b6e5a4b188cb183c7e59558cce36cb6 -->
 ## Display a listing of the resource.
 
 > Example request:
@@ -58,12 +58,10 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user`
 
-`HEAD api/user`
 
+<!-- END_2b6e5a4b188cb183c7e59558cce36cb6 -->
 
-<!-- END_2ea88ff35aa222f5582e50f39a2b35fd -->
-
-<!-- START_99a7210df460e7fd8ad2508ee28b9763 -->
+<!-- START_7f66c974d24032cb19061d55d801f62b -->
 ## Show the form for creating a new resource.
 
 > Example request:
@@ -100,10 +98,8 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/create`
 
-`HEAD api/user/create`
-
 
-<!-- END_99a7210df460e7fd8ad2508ee28b9763 -->
+<!-- END_7f66c974d24032cb19061d55d801f62b -->
 
 <!-- START_f0654d3f2fc63c11f5723f233cc53c83 -->
 ## Store a newly created resource in storage.
@@ -138,7 +134,7 @@ $.ajax(settings).done(function (response) {
 
 <!-- END_f0654d3f2fc63c11f5723f233cc53c83 -->
 
-<!-- START_7a5835399fad9a53bc0430d6e3054297 -->
+<!-- START_ceec0e0b1d13d731ad96603d26bccc2f -->
 ## Display the specified resource.
 
 > Example request:
@@ -175,12 +171,10 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/{user}`
 
-`HEAD api/user/{user}`
 
+<!-- END_ceec0e0b1d13d731ad96603d26bccc2f -->
 
-<!-- END_7a5835399fad9a53bc0430d6e3054297 -->
-
-<!-- START_5ed9d10b12650f9536edfa994fafae15 -->
+<!-- START_f4aa12af19ba08e1448d7eafc9f55e67 -->
 ## Show the form for editing the specified resource.
 
 > Example request:
@@ -217,10 +211,8 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/{user}/edit`
 
-`HEAD api/user/{user}/edit`
-
 
-<!-- END_5ed9d10b12650f9536edfa994fafae15 -->
+<!-- END_f4aa12af19ba08e1448d7eafc9f55e67 -->
 
 <!-- START_a4a2abed1e8e8cad5e6a3282812fe3f3 -->
 ## Update the specified resource in storage.

+ 6 - 6
tests/GenerateDocumentationTest.php

@@ -64,8 +64,8 @@ class GenerateDocumentationTest extends TestCase
         $output = $this->artisan('api:generate', [
             '--routePrefix' => 'api/*',
         ]);
-        $this->assertContains('Skipping route: [GET,HEAD] api/closure', $output);
-        $this->assertContains('Processed route: [GET,HEAD] api/test', $output);
+        $this->assertContains('Skipping route: [GET] api/closure', $output);
+        $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
     public function testConsoleCommandDoesNotWorkWithClosureUsingDingo()
@@ -85,8 +85,8 @@ class GenerateDocumentationTest extends TestCase
                 '--router' => 'dingo',
                 '--routePrefix' => 'v1',
             ]);
-            $this->assertContains('Skipping route: [GET,HEAD] closure', $output);
-            $this->assertContains('Processed route: [GET,HEAD] test', $output);
+            $this->assertContains('Skipping route: [GET] closure', $output);
+            $this->assertContains('Processed route: [GET] test', $output);
         });
     }
 
@@ -98,8 +98,8 @@ class GenerateDocumentationTest extends TestCase
         $output = $this->artisan('api:generate', [
             '--routePrefix' => 'api/*',
         ]);
-        $this->assertContains('Skipping route: [GET,HEAD] api/skip', $output);
-        $this->assertContains('Processed route: [GET,HEAD] api/test', $output);
+        $this->assertContains('Skipping route: [GET] api/skip', $output);
+        $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
     public function testCanParseResourceRoutes()