Skip to content

Commit 9583758

Browse files
authored
Merge pull request #61 from robertmarney/patch-1
Utilize Rules via FormRequest to populate description
2 parents 7eedfdc + 43e70c9 commit 9583758

File tree

7 files changed

+208
-9
lines changed

7 files changed

+208
-9
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
This package allows you to automatically generate a Postman collection based on your API routes. It also provides basic configuration and support for bearer auth tokens and basic auth for routes behind an auth middleware.
99

10+
For ```POST``` and ```PUT``` requests that utilizes a FormRequest, you can optionally scaffold the request, and publish rules in raw or human readable format.
1011
## Postman Schema
1112

1213
The generator works for the latest version of the Postman Schema at the time of publication (v2.1.0).

config/api-postman.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@
7979

8080
'enable_formdata' => false,
8181

82+
/*
83+
|--------------------------------------------------------------------------
84+
| Parse Form Request Rules
85+
|--------------------------------------------------------------------------
86+
|
87+
| If you want form requests to be printed in the field description field,
88+
| and if so, whether they will be in a human readable form.
89+
|
90+
*/
91+
92+
'print_rules' => true, // @requires: 'enable_formdata' === true
93+
'rules_to_human_readable' => true, // @requires: 'parse_rules' === true
94+
8295
/*
8396
|--------------------------------------------------------------------------
8497
| Form Data

src/Commands/ExportPostmanCommand.php

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
use Closure;
66
use Illuminate\Console\Command;
77
use Illuminate\Contracts\Config\Repository;
8+
use Illuminate\Contracts\Validation\Rule;
89
use Illuminate\Foundation\Http\FormRequest;
910
use Illuminate\Routing\Router;
1011
use Illuminate\Support\Facades\Storage;
12+
use Illuminate\Support\Facades\Validator;
1113
use Illuminate\Support\Str;
14+
use Illuminate\Validation\ValidationRuleParser;
1215
use ReflectionClass;
1316
use ReflectionFunction;
1417

1518
class ExportPostmanCommand extends Command
1619
{
1720
/** @var string */
18-
protected $signature = 'export:postman
21+
protected $signature = 'export:postman
1922
{--bearer= : The bearer token to use on your endpoints}
2023
{--basic= : The basic auth to use on your endpoints}';
2124

@@ -46,6 +49,9 @@ class ExportPostmanCommand extends Command
4649
'basic',
4750
];
4851

52+
/** @var \Illuminate\Validation\Validator */
53+
private $validator;
54+
4955
public function __construct(Router $router, Repository $config)
5056
{
5157
parent::__construct();
@@ -102,14 +108,22 @@ public function handle(): void
102108
$rules = method_exists($rulesParameter, 'rules') ? $rulesParameter->rules() : [];
103109

104110
foreach ($rules as $fieldName => $rule) {
105-
$requestRules[] = $fieldName;
106-
107111
if (is_string($rule)) {
108112
$rule = preg_split('/\s*\|\s*/', $rule);
109113
}
110114

115+
$printRules = $this->config['print_rules'];
116+
117+
$requestRules[] = [
118+
'name' => $fieldName,
119+
'description' => $printRules ? $rule : '',
120+
];
121+
111122
if (is_array($rule) && in_array('confirmed', $rule)) {
112-
$requestRules[] = $fieldName.'_confirmation';
123+
$requestRules[] = [
124+
'name' => $fieldName.'_confirmation',
125+
'description' => $printRules ? $rule : '',
126+
];
113127
}
114128
}
115129
}
@@ -233,6 +247,8 @@ protected function buildTree(array &$routes, array $segments, array $request): v
233247

234248
public function makeRequest($route, $method, $routeHeaders, $requestRules)
235249
{
250+
$printRules = $this->config['print_rules'];
251+
236252
$uri = Str::of($route->uri())->replaceMatches('/{([[:alnum:]]+)}/', ':$1');
237253

238254
$variables = $uri->matchAll('/(?<={)[[:alnum:]]+(?=})/m');
@@ -258,9 +274,10 @@ public function makeRequest($route, $method, $routeHeaders, $requestRules)
258274

259275
foreach ($requestRules as $rule) {
260276
$ruleData[] = [
261-
'key' => $rule,
262-
'value' => $this->config['formdata'][$rule] ?? null,
277+
'key' => $rule['name'],
278+
'value' => $this->config['formdata'][$rule['name']] ?? null,
263279
'type' => 'text',
280+
'description' => $printRules ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) : '',
264281
];
265282
}
266283

@@ -273,6 +290,55 @@ public function makeRequest($route, $method, $routeHeaders, $requestRules)
273290
return $data;
274291
}
275292

293+
/**
294+
* Process a rule set and utilize the Validator to create human readable descriptions
295+
* to help users provide valid data.
296+
*
297+
* @param $attribute
298+
* @param $rules
299+
* @return string
300+
*/
301+
protected function parseRulesIntoHumanReadable($attribute, $rules): string
302+
{
303+
304+
// ... bail if user has asked for non interpreted strings:
305+
if (! $this->config['rules_to_human_readable']) {
306+
return is_array($rules) ? implode(', ', $rules) : $this->safelyStringifyClassBasedRule($rules);
307+
}
308+
309+
/*
310+
* An object based rule is presumably a Laravel default class based rule or one that implements the Illuminate
311+
* Rule interface. Lets try to safely access the string representation...
312+
*/
313+
if (is_object($rules)) {
314+
$rules = [$this->safelyStringifyClassBasedRule($rules)];
315+
}
316+
317+
/*
318+
* Handle string based rules (e.g. required|string|max:30)
319+
*/
320+
if (is_array($rules)) {
321+
$this->validator = Validator::make([], [$attribute => implode('|', $rules)]);
322+
323+
foreach ($rules as $rule) {
324+
[$rule, $parameters] = ValidationRuleParser::parse($rule);
325+
326+
$this->validator->addFailure($attribute, $rule, $parameters);
327+
}
328+
329+
$messages = $this->validator->getMessageBag()->toArray()[$attribute];
330+
331+
if (is_array($messages)) {
332+
$messages = $this->handleEdgeCases($messages);
333+
}
334+
335+
return implode(', ', is_array($messages) ? $messages : $messages->toArray());
336+
}
337+
338+
// ...safely return a safe value if we encounter neither a string or object based rule set:
339+
return '';
340+
}
341+
276342
protected function initializeStructure(): void
277343
{
278344
$this->structure = [
@@ -320,4 +386,42 @@ protected function isStructured()
320386
{
321387
return $this->config['structured'];
322388
}
389+
390+
/**
391+
* Certain fields are not handled via the normal throw failure method in the validator
392+
* We need to add a human readable message.
393+
*
394+
* @param array $messages
395+
* @return array
396+
*/
397+
protected function handleEdgeCases(array $messages): array
398+
{
399+
foreach ($messages as $key => $message) {
400+
if ($message === 'validation.nullable') {
401+
$messages[$key] = '(Nullable)';
402+
continue;
403+
}
404+
405+
if ($message === 'validation.sometimes') {
406+
$messages[$key] = '(Optional)';
407+
}
408+
}
409+
410+
return $messages;
411+
}
412+
413+
/**
414+
* In this case we have received what is most likely a Rule Object but are not certain.
415+
*
416+
* @param $probableRule
417+
* @return string
418+
*/
419+
protected function safelyStringifyClassBasedRule($probableRule): string
420+
{
421+
if (is_object($probableRule) && (is_subclass_of($probableRule, Rule::class) || method_exists($probableRule, '__toString'))) {
422+
return (string) $probableRule;
423+
}
424+
425+
return '';
426+
}
323427
}

tests/Feature/ExportPostmanTest.php

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public function test_standard_export_works(bool $formDataEnabled)
3838
$collectionRoute = Arr::first($collectionItems, function ($item) use ($route) {
3939
return $item['name'] == $route->uri();
4040
});
41-
4241
$this->assertNotNull($collectionRoute);
4342
$this->assertTrue(in_array($collectionRoute['request']['method'], $route->methods()));
4443
}
@@ -77,7 +76,6 @@ public function test_bearer_export_works(bool $formDataEnabled)
7776
$collectionRoute = Arr::first($collectionItems, function ($item) use ($route) {
7877
return $item['name'] == $route->uri();
7978
});
80-
8179
$this->assertNotNull($collectionRoute);
8280
$this->assertTrue(in_array($collectionRoute['request']['method'], $route->methods()));
8381
}
@@ -116,7 +114,6 @@ public function test_basic_export_works(bool $formDataEnabled)
116114
$collectionRoute = Arr::first($collectionItems, function ($item) use ($route) {
117115
return $item['name'] == $route->uri();
118116
});
119-
120117
$this->assertNotNull($collectionRoute);
121118
$this->assertTrue(in_array($collectionRoute['request']['method'], $route->methods()));
122119
}
@@ -145,6 +142,63 @@ public function test_structured_export_works(bool $formDataEnabled)
145142
$this->assertCount(count($routes), $collectionItems[0]['item']);
146143
}
147144

145+
public function test_rules_printing_export_works()
146+
{
147+
config([
148+
'api-postman.enable_formdata' => true,
149+
'api-postman.print_rules' => true,
150+
'api-postman.rules_to_human_readable' => false,
151+
]);
152+
153+
$this->artisan('export:postman')->assertExitCode(0);
154+
155+
$this->assertTrue(true);
156+
157+
$collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
158+
159+
$targetRequest = $collection
160+
->where('name', 'example/storeWithFormRequest')
161+
->first();
162+
163+
$fields = collect($targetRequest['request']['body']['urlencoded']);
164+
$this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required'));
165+
$this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer'));
166+
$this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1'));
167+
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
168+
}
169+
170+
public function test_rules_printing_export_to_human_readable_works()
171+
{
172+
config([
173+
'api-postman.enable_formdata' => true,
174+
'api-postman.print_rules' => true,
175+
'api-postman.rules_to_human_readable' => true,
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/storeWithFormRequest')
186+
->first();
187+
188+
$fields = collect($targetRequest['request']['body']['urlencoded']);
189+
$this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'The field 1 field is required.'));
190+
$this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'The field 2 field is required., The field 2 must be an integer.'));
191+
$this->assertCount(1, $fields->where('key', 'field_3')->where('description', '(Optional), The field 3 must be an integer.'));
192+
$this->assertCount(1, $fields->where('key', 'field_4')->where('description', '(Nullable), The field 4 must be an integer.'));
193+
$this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'The field 5 field is required., The field 5 must be an integer., The field 5 must not be greater than 30., The field 5 must be at least 1.'));
194+
195+
/** This looks bad, but this is the default message in lang/en/validation.php, you can update to:.
196+
*
197+
* "'in' => 'The selected :attribute is invalid. Allowable values: :values',"
198+
**/
199+
$this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'The selected field 6 is invalid.'));
200+
}
201+
148202
public function providerFormDataEnabled(): array
149203
{
150204
return [

tests/Fixtures/ExampleController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ public function showWithReflectionMethod(ExampleService $service): array
3030
{
3131
return $service->getRequestData();
3232
}
33+
34+
public function storeWithFormRequest(ExampleFormRequest $request): string
35+
{
36+
return 'storeWithFormRequest';
37+
}
3338
}

tests/Fixtures/ExampleFormRequest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace AndreasElia\PostmanGenerator\Tests\Fixtures;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
use Illuminate\Validation\Rules\In;
7+
8+
class ExampleFormRequest extends FormRequest
9+
{
10+
public function rules(): array
11+
{
12+
return [
13+
'field_1' => 'required',
14+
'field_2' => 'required|integer',
15+
'field_3' => 'sometimes|integer',
16+
'field_4' => 'nullable|integer',
17+
'field_5' => 'required|integer|max:30|min:1',
18+
'field_6' => new In([1, 2, 3]),
19+
];
20+
}
21+
}

tests/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ protected function defineRoutes($router)
1919
$router->post('store', [ExampleController::class, 'store'])->name('store');
2020
$router->delete('delete', [ExampleController::class, 'delete'])->name('delete');
2121
$router->get('showWithReflectionMethod', [ExampleController::class, 'showWithReflectionMethod'])->name('show-with-reflection-method');
22+
$router->post('storeWithFormRequest', [ExampleController::class, 'storeWithFormRequest'])->name('store-with-form-request');
2223
});
2324
}
2425
}

0 commit comments

Comments
 (0)