Skip to content

Commit 2cbc360

Browse files
authored
Merge pull request #93 from Menkveld-24/master
Doc comments + GET query params
2 parents 35447ef + e768739 commit 2cbc360

File tree

9 files changed

+196
-41
lines changed

9 files changed

+196
-41
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0",
3434
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0",
3535
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0",
36-
"illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0"
36+
"illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0",
37+
"phpstan/phpdoc-parser": "^1.24"
3738
},
3839
"extra": {
3940
"laravel": {

config/api-postman.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@
8484
'prerequest_script' => '', // This script will execute before every request in the collection.
8585
'test_script' => '', // This script will execute after every request in the collection.
8686

87+
/*
88+
|--------------------------------------------------------------------------
89+
| Include Doc Comments
90+
|--------------------------------------------------------------------------
91+
|
92+
| Determines whether or not to set the PHP Doc comments to the description
93+
| in postman.
94+
|
95+
*/
96+
97+
'include_doc_comments' => false,
98+
8799
/*
88100
|--------------------------------------------------------------------------
89101
| Enable Form Data

phpunit.xml

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit
3-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4-
bootstrap="vendor/autoload.php"
5-
backupGlobals="false"
6-
colors="true"
7-
processIsolation="false"
8-
stopOnFailure="false"
9-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
10-
cacheDirectory=".phpunit.cache"
11-
backupStaticProperties="false"
12-
>
13-
<coverage>
14-
<include>
15-
<directory suffix=".php">src/</directory>
16-
</include>
17-
</coverage>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
3+
<coverage/>
184
<testsuites>
195
<testsuite name="Unit">
206
<directory suffix="Test.php">./tests/Unit</directory>
@@ -26,4 +12,9 @@
2612
<php>
2713
<env name="DB_CONNECTION" value="testing"/>
2814
</php>
15+
<source>
16+
<include>
17+
<directory suffix=".php">src/</directory>
18+
</include>
19+
</source>
2920
</phpunit>

phpunit.xml.bak

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
bootstrap="vendor/autoload.php"
5+
backupGlobals="false"
6+
colors="true"
7+
processIsolation="false"
8+
stopOnFailure="false"
9+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
10+
cacheDirectory=".phpunit.cache"
11+
backupStaticProperties="false"
12+
>
13+
<coverage>
14+
<include>
15+
<directory suffix=".php">src/</directory>
16+
</include>
17+
</coverage>
18+
<testsuites>
19+
<testsuite name="Unit">
20+
<directory suffix="Test.php">./tests/Unit</directory>
21+
</testsuite>
22+
<testsuite name="Feature">
23+
<directory suffix="Test.php">./tests/Feature</directory>
24+
</testsuite>
25+
</testsuites>
26+
<php>
27+
<env name="DB_CONNECTION" value="testing"/>
28+
</php>
29+
</phpunit>

src/Commands/ExportPostmanCommand.php

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
use Illuminate\Console\Command;
77
use Illuminate\Contracts\Config\Repository;
88
use Illuminate\Contracts\Validation\Rule;
9+
use Illuminate\Contracts\Validation\ValidationRule;
910
use Illuminate\Foundation\Http\FormRequest;
1011
use Illuminate\Routing\Router;
1112
use Illuminate\Support\Facades\Storage;
1213
use Illuminate\Support\Facades\Validator;
1314
use Illuminate\Support\Str;
1415
use Illuminate\Validation\ValidationRuleParser;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
17+
use PHPStan\PhpDocParser\Lexer\Lexer;
18+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
19+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
20+
use PHPStan\PhpDocParser\Parser\TokenIterator;
21+
use PHPStan\PhpDocParser\Parser\TypeParser;
1522
use ReflectionClass;
1623
use ReflectionFunction;
1724

@@ -43,6 +50,12 @@ class ExportPostmanCommand extends Command
4350
/** @var string */
4451
private $authType;
4552

53+
/** @var Lexer */
54+
private Lexer $lexer;
55+
56+
/** @var PhpDocParser */
57+
private PhpDocParser $phpDocParser;
58+
4659
/** @var array */
4760
private const AUTH_OPTIONS = [
4861
'bearer',
@@ -65,6 +78,7 @@ public function handle(): void
6578
$this->setFilename();
6679
$this->setAuthToken();
6780
$this->initializeStructure();
81+
$this->initializePhpDocParser();
6882

6983
foreach ($this->router->getRoutes() as $route) {
7084
$methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD');
@@ -85,6 +99,8 @@ public function handle(): void
8599

86100
$requestRules = [];
87101

102+
$requestDescription = '';
103+
88104
$routeAction = $route->getAction();
89105

90106
$reflectionMethod = $this->getReflectionMethod($routeAction);
@@ -93,6 +109,23 @@ public function handle(): void
93109
continue;
94110
}
95111

112+
if ($this->config['include_doc_comments']) {
113+
try {
114+
$docComment = $reflectionMethod->getDocComment();
115+
$tokens = new TokenIterator($this->lexer->tokenize($docComment));
116+
$phpDocNode = $this->phpDocParser->parse($tokens);
117+
118+
foreach ($phpDocNode->children as $child) {
119+
if ($child instanceof PhpDocTextNode) {
120+
$requestDescription .= ' '.$child->text;
121+
}
122+
}
123+
$requestDescription = Str::squish($requestDescription);
124+
} catch (\Exception $e) {
125+
$this->warn('Error at parsing phpdoc at '.$reflectionMethod->class.'::'.$reflectionMethod->name);
126+
}
127+
}
128+
96129
if ($this->config['enable_formdata']) {
97130
$rulesParameter = collect($reflectionMethod->getParameters())
98131
->filter(function ($value, $key) {
@@ -149,7 +182,7 @@ public function handle(): void
149182
}
150183
}
151184

152-
$request = $this->makeRequest($route, $method, $routeHeaders, $requestRules);
185+
$request = $this->makeRequest($route, $method, $routeHeaders, $requestRules, $requestDescription);
153186

154187
if ($this->isStructured()) {
155188
$routeNames = $route->action['as'] ?? null;
@@ -258,7 +291,7 @@ protected function buildTree(array &$routes, array $segments, array $request): v
258291
}
259292
}
260293

261-
public function makeRequest($route, $method, $routeHeaders, $requestRules)
294+
public function makeRequest($route, $method, $routeHeaders, $requestRules, $requestDescription)
262295
{
263296
$printRules = $this->config['print_rules'];
264297

@@ -279,6 +312,7 @@ public function makeRequest($route, $method, $routeHeaders, $requestRules)
279312
return ['key' => $variable, 'value' => ''];
280313
})->all(),
281314
],
315+
'description' => $requestDescription,
282316
],
283317
];
284318

@@ -294,10 +328,18 @@ public function makeRequest($route, $method, $routeHeaders, $requestRules)
294328
];
295329
}
296330

297-
$data['request']['body'] = [
298-
'mode' => 'urlencoded',
299-
'urlencoded' => $ruleData,
300-
];
331+
if ($method === 'GET') {
332+
foreach ($ruleData as &$rule) {
333+
unset($rule['type']);
334+
$rule['disabled'] = false;
335+
}
336+
$data['request']['url']['query'] = $ruleData;
337+
} else {
338+
$data['request']['body'] = [
339+
'mode' => 'urlencoded',
340+
'urlencoded' => $ruleData,
341+
];
342+
}
301343
}
302344

303345
return $data;
@@ -317,7 +359,7 @@ protected function parseRulesIntoHumanReadable($attribute, $rules): string
317359
if (! $this->config['rules_to_human_readable']) {
318360
foreach ($rules as $i => $rule) {
319361
// because we don't support custom rule classes, we remove them from the rules
320-
if (is_subclass_of($rule, Rule::class)) {
362+
if (is_subclass_of($rule, Rule::class) || is_subclass_of($rule, ValidationRule::class)) {
321363
unset($rules[$i]);
322364
}
323365
}
@@ -364,6 +406,17 @@ protected function parseRulesIntoHumanReadable($attribute, $rules): string
364406
return '';
365407
}
366408

409+
/**
410+
* Initializes the phpDocParser and lexer.
411+
*/
412+
protected function initializePhpDocParser(): void
413+
{
414+
$this->lexer = new Lexer();
415+
$constExprParser = new ConstExprParser();
416+
$typeParser = new TypeParser($constExprParser);
417+
$this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
418+
}
419+
367420
protected function initializeStructure(): void
368421
{
369422
$this->structure = [
@@ -471,7 +524,7 @@ protected function handleEdgeCases(array $messages): array
471524
*/
472525
protected function safelyStringifyClassBasedRule($probableRule): string
473526
{
474-
if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) {
527+
if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || is_subclass_of($probableRule, ValidationRule::class) || ! method_exists($probableRule, '__toString')) {
475528
return '';
476529
}
477530

tests/Feature/ExportPostmanTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,41 @@ public function test_rules_printing_export_works()
167167
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
168168
}
169169

170+
public function test_rules_printing_get_export_works()
171+
{
172+
config([
173+
'api-postman.enable_formdata' => true,
174+
'api-postman.print_rules' => true,
175+
'api-postman.rules_to_human_readable' => false,
176+
]);
177+
178+
$this->artisan('export:postman')->assertExitCode(0);
179+
180+
$this->assertTrue(true);
181+
182+
$collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
183+
184+
$targetRequest = $collection
185+
->where('name', 'example/getWithFormRequest')
186+
->first();
187+
188+
$fields = collect($targetRequest['request']['url']['query']);
189+
$this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required'));
190+
$this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer'));
191+
$this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1'));
192+
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
193+
194+
// Check for the required structure of the get request query
195+
foreach ($fields as $field) {
196+
$this->assertEqualsCanonicalizing([
197+
'key' => $field['key'],
198+
'value' => null,
199+
'disabled' => false,
200+
'description' => $field['description']
201+
], $field);
202+
}
203+
}
204+
170205
public function test_rules_printing_export_to_human_readable_works()
171206
{
172207
config([
@@ -231,6 +266,25 @@ public function test_event_export_works()
231266
}
232267
}
233268

269+
public function test_php_doc_comment_export()
270+
{
271+
config([
272+
'api-postman.include_doc_comments' => true,
273+
]);
274+
275+
$this->artisan('export:postman')->assertExitCode(0);
276+
277+
$this->assertTrue(true);
278+
279+
$collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
280+
281+
$targetRequest = $collection
282+
->where('name', 'example/phpDocRoute')
283+
->first();
284+
285+
$this->assertEquals($targetRequest['request']['description'], 'This is the php doc route. Which is also multi-line. and has a blank line.');
286+
}
287+
234288
public static function providerFormDataEnabled(): array
235289
{
236290
return [

tests/Fixtures/ExampleController.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,23 @@ public function storeWithFormRequest(ExampleFormRequest $request): string
3535
{
3636
return 'storeWithFormRequest';
3737
}
38+
39+
public function getWithFormRequest(ExampleFormRequest $request): string
40+
{
41+
return 'getWithFormRequest';
42+
}
43+
44+
/**
45+
* This is the php doc route.
46+
* Which is also multi-line.
47+
*
48+
* and has a blank line.
49+
*
50+
* @param string $non-existing param
51+
* @return string
52+
*/
53+
public function phpDocRoute(): string
54+
{
55+
return 'phpDocRoute';
56+
}
3857
}

tests/Fixtures/UppercaseRule.php

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,23 @@
22

33
namespace AndreasElia\PostmanGenerator\Tests\Fixtures;
44

5-
use Illuminate\Contracts\Validation\Rule;
5+
use Illuminate\Contracts\Validation\ValidationRule;
6+
use Closure;
67

7-
class UppercaseRule implements Rule
8+
class UppercaseRule implements ValidationRule
89
{
910
/**
10-
* Determine if the validation rule passes.
11+
* Run the validation rule.
1112
*
1213
* @param string $attribute
1314
* @param mixed $value
14-
* @return bool
15+
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
16+
* @return void
1517
*/
16-
public function passes($attribute, $value)
18+
public function validate(string $attribute, mixed $value, Closure $fail): void
1719
{
18-
return strtoupper($value) === $value;
19-
}
20-
21-
/**
22-
* Get the validation error message.
23-
*
24-
* @return string
25-
*/
26-
public function message()
27-
{
28-
return 'The :attribute must be uppercase.';
20+
if (strtoupper($value) !== $value) {
21+
$fail("The {$attribute} must be uppercase.");
22+
}
2923
}
3024
}

tests/TestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ protected function defineRoutes($router)
2020
$router->delete('delete', [ExampleController::class, 'delete'])->name('delete');
2121
$router->get('showWithReflectionMethod', [ExampleController::class, 'showWithReflectionMethod'])->name('show-with-reflection-method');
2222
$router->post('storeWithFormRequest', [ExampleController::class, 'storeWithFormRequest'])->name('store-with-form-request');
23+
$router->get('getWithFormRequest', [ExampleController::class, 'getWithFormRequest'])->name('get-with-form-request');
24+
$router->get('phpDocRoute', [ExampleController::class, 'phpDocRoute'])->name('php-doc-route');
2325
});
2426
}
2527
}

0 commit comments

Comments
 (0)