Skip to content

Commit 6cb7a6a

Browse files
committed
ability to add descriptions
1 parent 1bc333b commit 6cb7a6a

File tree

6 files changed

+175
-13
lines changed

6 files changed

+175
-13
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Laravel Swagger
22

3-
This package scans your laravel project's routes and auto generates a Swagger 2.0 documentation for you. If you inject Form Request classes in your controller's actions as request validation, it will also generate the parameters for each request that has them. It will take into account wether the request is a GET/HEAD/DELETE or a POST/PUT/PATCH request and make its best guess as to the type of parameter object it should generate. It will also generate the path parameters if you route contains them.
3+
This package scans your laravel project's routes and auto generates a Swagger 2.0 documentation for you. If you inject Form Request classes in your controller's actions as request validation, it will also generate the parameters for each request that has them. It will take into account wether the request is a GET/HEAD/DELETE or a POST/PUT/PATCH request and make its best guess as to the type of parameter object it should generate. It will also generate the path parameters if you route contains them. Finally, this package will also scan any documentation you have in your action methods and add it as description to that path, along with any appropriate annotations such as @deprecated.
44

55
## Installation
66

@@ -30,6 +30,11 @@ Say you have a route `/api/users/{id}` that maps to `UserController@show`
3030

3131
Your sample controller might look like this:
3232
```php
33+
/**
34+
* Return all the details of a user
35+
*
36+
* @deprecated
37+
*/
3338
class UserController extends Controller
3439
{
3540
public function show(UserShowRequest $request, $id)
@@ -68,7 +73,9 @@ Running `php artisan laravel-swagger:generate > swagger.json` will generate the
6873
"paths": {
6974
"\/api\/user\/{id}": {
7075
"get": {
71-
"description": "GET \/api\/user\/{id}",
76+
"summary": "GET \/api\/user\/{id}",
77+
"description": "Return all the details of a user",
78+
"deprecated": true
7279
"responses": {
7380
"200": {
7481
"description": "OK"

config/laravel-swagger.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,16 @@
4747
'ignoredMethods' => [
4848
'head',
4949
],
50+
51+
/*
52+
|--------------------------------------------------------------------------
53+
| Include descriptions
54+
|--------------------------------------------------------------------------
55+
|
56+
| If true will parse the action method docBlock for any lines that are not
57+
| part of an annotation and will add it as the route's description
58+
|
59+
*/
60+
61+
'parseDescriptions' => true,
5062
];

src/Generator.php

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,16 @@ protected function getRouteUri(Route $route)
108108

109109
protected function generatePath()
110110
{
111-
$methodDescription = strtoupper($this->method);
111+
$requestMethod = strtoupper($this->method);
112+
$actionInstance = $this->action ? $this->getActionClassInstance($this->action) : null;
113+
$docBlock = $actionInstance ? ($actionInstance->getDocComment() ?: "") : "";
114+
115+
list($isDeprecated, $description) = $this->parseActionDocBlock($docBlock);
112116

113117
$this->docs['paths'][$this->uri][$this->method] = [
114-
'description' => "$methodDescription {$this->uri}",
118+
'summary' => "$requestMethod {$this->uri}",
119+
'description' => $description,
120+
'deprecated' => $isDeprecated,
115121
'responses' => [
116122
'200' => [
117123
'description' => 'OK'
@@ -143,9 +149,7 @@ protected function getFormRules()
143149
{
144150
if (!is_string($this->action)) return false;
145151

146-
$parsedAction = Str::parseCallback($this->action);
147-
148-
$parameters = (new ReflectionMethod($parsedAction[0], $parsedAction[1]))->getParameters();
152+
$parameters = $this->getActionClassInstance($this->action)->getParameters();
149153

150154
foreach ($parameters as $parameter) {
151155
$class = (string) $parameter->getType();
@@ -167,4 +171,82 @@ protected function getParameterGenerator($rules)
167171
return new Parameters\QueryParameterGenerator($rules);
168172
}
169173
}
174+
175+
private function getActionClassInstance(string $action)
176+
{
177+
list($class, $method) = Str::parseCallback($action);
178+
179+
return new ReflectionMethod($class, $method);
180+
}
181+
182+
private function parseActionDocBlock(string $docBlock)
183+
{
184+
$isDeprecated = !!preg_match('/@deprecated/', $docBlock);
185+
186+
preg_match('/\/\*\s*(.+?)\s*\*\//sm', $docBlock, $matches);
187+
$commentWithoutTrailingCharacters = isset($matches[1]) ? $matches[1] : "";
188+
189+
if (!$this->config['parseDescriptions']) {
190+
$commentWithoutTrailingCharacters = "";
191+
}
192+
193+
if (empty($commentWithoutTrailingCharacters)) {
194+
return [$isDeprecated, $commentWithoutTrailingCharacters];
195+
}
196+
197+
$lines = explode("\n", $commentWithoutTrailingCharacters);
198+
$lines = array_map('trim', $lines);
199+
200+
$description = $this->buildDescription($lines);
201+
202+
return [$isDeprecated, $description];
203+
}
204+
205+
private function buildDescription(array $matches)
206+
{
207+
$commentLines = $this->getNonAnnotationLines($matches);
208+
$commentLines = $this->trimLines($commentLines);
209+
$commentLines = $this->trimAroundComment($commentLines);
210+
211+
return implode("\n", $commentLines);
212+
}
213+
214+
private function getNonAnnotationLines(array $lines)
215+
{
216+
return array_filter($lines, function($line) {
217+
return !preg_match('/^\*+\s*@/', $line);
218+
});
219+
}
220+
221+
private function trimLines(array $comments)
222+
{
223+
$comments = array_map(function($line) {
224+
preg_match('/^\**(.*)/', $line, $matches);
225+
226+
return isset($matches[1]) ? $matches[1] : "\n";
227+
}, $comments);
228+
229+
return array_map('trim', $comments);
230+
}
231+
232+
private function trimAroundComment(array $lines)
233+
{
234+
foreach ($lines as $key => $value) {
235+
if (trim($value)) {
236+
break;
237+
}
238+
239+
unset($lines[$key]);
240+
}
241+
242+
foreach (array_reverse($lines, true) as $key => $value) {
243+
if (trim($value)) {
244+
break;
245+
}
246+
247+
unset($lines[$key]);
248+
}
249+
250+
return array_values($lines);
251+
}
170252
}

tests/GeneratorTest.php

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function testHasPaths($docs)
4343
$this->assertEquals([
4444
'/users',
4545
'/users/{id}',
46+
'/users/details',
4647
'/api',
4748
'/api/store',
4849
], array_keys($docs['paths']));
@@ -53,19 +54,51 @@ public function testHasPaths($docs)
5354
/**
5455
* @depends testHasPaths
5556
*/
56-
public function testPathData($paths)
57+
public function testPathMethods($paths)
5758
{
5859
$this->assertArrayHasKey('get', $paths['/users']);
5960
$this->assertArrayNotHasKey('head', $paths['/users']);
6061
$this->assertArrayHasKey('post', $paths['/users']);
6162

63+
$this->assertArrayHasKey('get', $paths['/users/{id}']);
64+
65+
$this->assertArrayHasKey('get', $paths['/users/details']);
66+
}
67+
68+
/**
69+
* @depends testHasPaths
70+
*/
71+
public function testRouteData($paths)
72+
{
73+
74+
$expectedPostDescription = <<<EOD
75+
Store a new user in the application
76+
Data is validated [see description here](https://example.com) so no bad data can be passed.
77+
78+
Please read the documentation for more information
79+
EOD;
80+
81+
$this->assertArrayHasKey('summary', $paths['/users']['get']);
6282
$this->assertArrayHasKey('description', $paths['/users']['get']);
6383
$this->assertArrayHasKey('responses', $paths['/users']['get']);
84+
$this->assertArrayHasKey('deprecated', $paths['/users']['get']);
6485
$this->assertArrayNotHasKey('parameters', $paths['/users']['get']);
6586

66-
$this->assertArrayHasKey('description', $paths['/users']['post']);
67-
$this->assertArrayHasKey('responses', $paths['/users']['post']);
68-
$this->assertArrayHasKey('parameters', $paths['/users']['post']);
87+
$this->assertEquals('GET /users', $paths['/users']['get']['summary']);
88+
$this->assertEquals(false, $paths['/users']['get']['deprecated']);
89+
$this->assertEquals('Get a list of of users in the application', $paths['/users']['get']['description']);
90+
91+
$this->assertEquals('POST /users', $paths['/users']['post']['summary']);
92+
$this->assertEquals(true, $paths['/users']['post']['deprecated']);
93+
$this->assertEquals($expectedPostDescription, $paths['/users']['post']['description']);
94+
95+
$this->assertEquals('GET /users/{id}', $paths['/users/{id}']['get']['summary']);
96+
$this->assertEquals(false, $paths['/users/{id}']['get']['deprecated']);
97+
$this->assertEquals("", $paths['/users/{id}']['get']['description']);
98+
99+
$this->assertEquals('GET /users/details', $paths['/users/details']['get']['summary']);
100+
$this->assertEquals(true, $paths['/users/details']['get']['deprecated']);
101+
$this->assertEquals("", $paths['/users/details']['get']['description']);
69102
}
70103

71104
public function testOverwriteIgnoreMethods()
@@ -77,6 +110,15 @@ public function testOverwriteIgnoreMethods()
77110
$this->assertArrayHasKey('head', $docs['paths']['/users']);
78111
}
79112

113+
public function testParseDescriptionFalse()
114+
{
115+
$this->config['parseDescriptions'] = false;
116+
117+
$docs = (new Generator($this->config))->generate();
118+
119+
$this->assertEquals("", $docs['paths']['/users']['post']['description']);
120+
}
121+
80122
public function testOptionalData()
81123
{
82124
$optionalData = [
@@ -111,7 +153,7 @@ public function testOptionalData()
111153
/**
112154
* @param string|null $routeFilter
113155
* @param array $expectedRoutes
114-
*
156+
*
115157
* @dataProvider filtersRoutesProvider
116158
*/
117159
public function testFiltersRoutes($routeFilter, $expectedRoutes)
@@ -132,7 +174,7 @@ public function testFiltersRoutes($routeFilter, $expectedRoutes)
132174
public function filtersRoutesProvider()
133175
{
134176
return [
135-
'No Filter' => [null, ['/users', '/users/{id}', '/api', '/api/store']],
177+
'No Filter' => [null, ['/users', '/users/{id}', '/users/details', '/api', '/api/store']],
136178
'/api Filter' => ['/api', ['/api', '/api/store']],
137179
'/=nonexistant Filter' => ['/nonexistant', []],
138180
];

tests/Stubs/Controllers/UserController.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
class UserController extends Controller
1010
{
11+
/** Get a list of of users in the application */
1112
public function index()
1213
{
1314
return json_encode([['first_name' => 'John'], ['first_name' => 'Jack']]);
@@ -18,8 +19,25 @@ public function show(UserShowRequest $request, $id)
1819
return json_encode(['first_name' => 'John']);
1920
}
2021

22+
/**
23+
* Store a new user in the application
24+
* Data is validated [see description here](https://example.com) so no bad data can be passed.
25+
*
26+
* Please read the documentation for more information
27+
*
28+
* @param UserStoreRequest $request
29+
* @deprecated
30+
*/
2131
public function store(UserStoreRequest $request)
2232
{
2333
return json_encode($request->all());
2434
}
35+
36+
/**
37+
* @deprecated
38+
*/
39+
public function details()
40+
{
41+
return json_encode([]);
42+
}
2543
}

tests/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ protected function getEnvironmentSetUp($app)
1616
$app['router']->get('/users', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\UserController@index');
1717
$app['router']->get('/users/{id}', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\UserController@show');
1818
$app['router']->post('/users', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\UserController@store');
19+
$app['router']->get('/users/details', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\UserController@details');
1920
$app['router']->get('/api', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\ApiController@index');
2021
$app['router']->put('/api/store', 'Mtrajano\\LaravelSwagger\\Tests\\Stubs\\Controllers\\ApiController@store');
2122
}

0 commit comments

Comments
 (0)