Skip to content

Doc comments + GET query params #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0"
"illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0",
"phpstan/phpdoc-parser": "^1.24"
},
"extra": {
"laravel": {
Expand Down
12 changes: 12 additions & 0 deletions config/api-postman.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@
'prerequest_script' => '', // This script will execute before every request in the collection.
'test_script' => '', // This script will execute after every request in the collection.

/*
|--------------------------------------------------------------------------
| Include Doc Comments
|--------------------------------------------------------------------------
|
| Determines whether or not to set the PHP Doc comments to the description
| in postman.
|
*/

'include_doc_comments' => false,

/*
|--------------------------------------------------------------------------
| Enable Form Data
Expand Down
23 changes: 7 additions & 16 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.0/phpunit.xsd"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false"
>
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<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">
<coverage/>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
Expand All @@ -26,4 +12,9 @@
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
29 changes: 29 additions & 0 deletions phpunit.xml.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.0/phpunit.xsd"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false"
>
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
69 changes: 61 additions & 8 deletions src/Commands/ExportPostmanCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationRuleParser;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use ReflectionClass;
use ReflectionFunction;

Expand Down Expand Up @@ -43,6 +50,12 @@ class ExportPostmanCommand extends Command
/** @var string */
private $authType;

/** @var Lexer */
private Lexer $lexer;

/** @var PhpDocParser */
private PhpDocParser $phpDocParser;

/** @var array */
private const AUTH_OPTIONS = [
'bearer',
Expand All @@ -65,6 +78,7 @@ public function handle(): void
$this->setFilename();
$this->setAuthToken();
$this->initializeStructure();
$this->initializePhpDocParser();

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

$requestRules = [];

$requestDescription = '';

$routeAction = $route->getAction();

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

if ($this->config['include_doc_comments']) {
try {
$docComment = $reflectionMethod->getDocComment();
$tokens = new TokenIterator($this->lexer->tokenize($docComment));
$phpDocNode = $this->phpDocParser->parse($tokens);

foreach ($phpDocNode->children as $child) {
if ($child instanceof PhpDocTextNode) {
$requestDescription .= ' '.$child->text;
}
}
$requestDescription = Str::squish($requestDescription);
} catch (\Exception $e) {
$this->warn('Error at parsing phpdoc at '.$reflectionMethod->class.'::'.$reflectionMethod->name);
}
}

if ($this->config['enable_formdata']) {
$rulesParameter = collect($reflectionMethod->getParameters())
->filter(function ($value, $key) {
Expand Down Expand Up @@ -149,7 +182,7 @@ public function handle(): void
}
}

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

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

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

Expand All @@ -279,6 +312,7 @@ public function makeRequest($route, $method, $routeHeaders, $requestRules)
return ['key' => $variable, 'value' => ''];
})->all(),
],
'description' => $requestDescription,
],
];

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

$data['request']['body'] = [
'mode' => 'urlencoded',
'urlencoded' => $ruleData,
];
if ($method === 'GET') {
foreach ($ruleData as &$rule) {
unset($rule['type']);
$rule['disabled'] = false;
}
$data['request']['url']['query'] = $ruleData;
} else {
$data['request']['body'] = [
'mode' => 'urlencoded',
'urlencoded' => $ruleData,
];
}
}

return $data;
Expand All @@ -317,7 +359,7 @@ protected function parseRulesIntoHumanReadable($attribute, $rules): string
if (! $this->config['rules_to_human_readable']) {
foreach ($rules as $i => $rule) {
// because we don't support custom rule classes, we remove them from the rules
if (is_subclass_of($rule, Rule::class)) {
if (is_subclass_of($rule, Rule::class) || is_subclass_of($rule, ValidationRule::class)) {
unset($rules[$i]);
}
}
Expand Down Expand Up @@ -364,6 +406,17 @@ protected function parseRulesIntoHumanReadable($attribute, $rules): string
return '';
}

/**
* Initializes the phpDocParser and lexer.
*/
protected function initializePhpDocParser(): void
{
$this->lexer = new Lexer();
$constExprParser = new ConstExprParser();
$typeParser = new TypeParser($constExprParser);
$this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
}

protected function initializeStructure(): void
{
$this->structure = [
Expand Down Expand Up @@ -471,7 +524,7 @@ protected function handleEdgeCases(array $messages): array
*/
protected function safelyStringifyClassBasedRule($probableRule): string
{
if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) {
if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || is_subclass_of($probableRule, ValidationRule::class) || ! method_exists($probableRule, '__toString')) {
return '';
}

Expand Down
54 changes: 54 additions & 0 deletions tests/Feature/ExportPostmanTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ public function test_rules_printing_export_works()
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
}

public function test_rules_printing_get_export_works()
{
config([
'api-postman.enable_formdata' => true,
'api-postman.print_rules' => true,
'api-postman.rules_to_human_readable' => false,
]);

$this->artisan('export:postman')->assertExitCode(0);

$this->assertTrue(true);

$collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);

$targetRequest = $collection
->where('name', 'example/getWithFormRequest')
->first();

$fields = collect($targetRequest['request']['url']['query']);
$this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required'));
$this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer'));
$this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1'));
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));

// Check for the required structure of the get request query
foreach ($fields as $field) {
$this->assertEqualsCanonicalizing([
'key' => $field['key'],
'value' => null,
'disabled' => false,
'description' => $field['description']
], $field);
}
}

public function test_rules_printing_export_to_human_readable_works()
{
config([
Expand Down Expand Up @@ -231,6 +266,25 @@ public function test_event_export_works()
}
}

public function test_php_doc_comment_export()
{
config([
'api-postman.include_doc_comments' => true,
]);

$this->artisan('export:postman')->assertExitCode(0);

$this->assertTrue(true);

$collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);

$targetRequest = $collection
->where('name', 'example/phpDocRoute')
->first();

$this->assertEquals($targetRequest['request']['description'], 'This is the php doc route. Which is also multi-line. and has a blank line.');
}

public static function providerFormDataEnabled(): array
{
return [
Expand Down
19 changes: 19 additions & 0 deletions tests/Fixtures/ExampleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,23 @@ public function storeWithFormRequest(ExampleFormRequest $request): string
{
return 'storeWithFormRequest';
}

public function getWithFormRequest(ExampleFormRequest $request): string
{
return 'getWithFormRequest';
}

/**
* This is the php doc route.
* Which is also multi-line.
*
* and has a blank line.
*
* @param string $non-existing param
* @return string
*/
public function phpDocRoute(): string
{
return 'phpDocRoute';
}
}
26 changes: 10 additions & 16 deletions tests/Fixtures/UppercaseRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,23 @@

namespace AndreasElia\PostmanGenerator\Tests\Fixtures;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidationRule;
use Closure;

class UppercaseRule implements Rule
class UppercaseRule implements ValidationRule
{
/**
* Determine if the validation rule passes.
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @return bool
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
* @return void
*/
public function passes($attribute, $value)
public function validate(string $attribute, mixed $value, Closure $fail): void
{
return strtoupper($value) === $value;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The :attribute must be uppercase.';
if (strtoupper($value) !== $value) {
$fail("The {$attribute} must be uppercase.");
}
}
}
2 changes: 2 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ protected function defineRoutes($router)
$router->delete('delete', [ExampleController::class, 'delete'])->name('delete');
$router->get('showWithReflectionMethod', [ExampleController::class, 'showWithReflectionMethod'])->name('show-with-reflection-method');
$router->post('storeWithFormRequest', [ExampleController::class, 'storeWithFormRequest'])->name('store-with-form-request');
$router->get('getWithFormRequest', [ExampleController::class, 'getWithFormRequest'])->name('get-with-form-request');
$router->get('phpDocRoute', [ExampleController::class, 'phpDocRoute'])->name('php-doc-route');
});
}
}