Skip to content

Commit 6770e67

Browse files
committed
Expanding Postman collection support
Along with resolving the `PostmanCollectionWriter` class with the application container, this PR extends support for Postman collections with the following features: - Properly handle url parameters using the prefixed colon syntax (opposed to the Laravel-esque `{param}` syntax) - Allow configuring the auth section of Postman collection config so your collection can indicate how to auth across all endpoints - Resolve query params from url params if the URL param is not visible in the route path - Add some documentation that was available but not exported in the Postman collection The PostmanCollectionWriter has been significantly refactored and cleaned up, and a full suite of tests have been added for it.
1 parent 027a429 commit 6770e67

File tree

4 files changed

+463
-41
lines changed

4 files changed

+463
-41
lines changed

config/apidoc.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
* The description for the exported Postman collection.
4242
*/
4343
'description' => null,
44+
45+
/*
46+
* The "Auth" section that should appear in the postman collection. See the schema docs for more information:
47+
* https://schema.getpostman.com/json/collection/v2.0.0/docs/index.html
48+
*/
49+
'auth' => null,
4450
],
4551

4652
/*

src/Writing/PostmanCollectionWriter.php

Lines changed: 132 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ class PostmanCollectionWriter
1919
*/
2020
private $baseUrl;
2121

22+
/**
23+
* @var string
24+
*/
25+
private $protocol;
26+
27+
/**
28+
* @var array|null
29+
*/
30+
private $auth;
31+
2232
/**
2333
* CollectionWriter constructor.
2434
*
@@ -27,16 +37,13 @@ class PostmanCollectionWriter
2737
public function __construct(Collection $routeGroups, $baseUrl)
2838
{
2939
$this->routeGroups = $routeGroups;
30-
$this->baseUrl = $baseUrl;
40+
$this->protocol = Str::startsWith($baseUrl, 'https') ? 'https' : 'http';
41+
$this->baseUrl = URL::formatRoot('', $baseUrl);
42+
$this->auth = config('apidoc.postman.auth');
3143
}
3244

3345
public function getCollection()
3446
{
35-
URL::forceRootUrl($this->baseUrl);
36-
if (Str::startsWith($this->baseUrl, 'https://')) {
37-
URL::forceScheme('https');
38-
}
39-
4047
$collection = [
4148
'variables' => [],
4249
'info' => [
@@ -45,46 +52,131 @@ public function getCollection()
4552
'description' => config('apidoc.postman.description') ?: '',
4653
'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
4754
],
48-
'item' => $this->routeGroups->map(function ($routes, $groupName) {
55+
'item' => $this->routeGroups->map(function (Collection $routes, $groupName) {
4956
return [
5057
'name' => $groupName,
51-
'description' => '',
52-
'item' => $routes->map(function ($route) {
53-
$mode = 'raw';
54-
55-
return [
56-
'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : url($route['uri']),
57-
'request' => [
58-
'url' => url($route['uri']).(collect($route['queryParameters'])->isEmpty()
59-
? ''
60-
: ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) {
61-
return urlencode($key).'='.urlencode($parameter['value'] ?? '');
62-
})->all()))),
63-
'method' => $route['methods'][0],
64-
'header' => collect($route['headers'])
65-
->union([
66-
'Accept' => 'application/json',
67-
])
68-
->map(function ($value, $header) {
69-
return [
70-
'key' => $header,
71-
'value' => $value,
72-
];
73-
})
74-
->values()->all(),
75-
'body' => [
76-
'mode' => $mode,
77-
$mode => json_encode($route['cleanBodyParameters'], JSON_PRETTY_PRINT),
78-
],
79-
'description' => $route['metadata']['description'],
80-
'response' => [],
81-
],
82-
];
83-
})->toArray(),
58+
'description' => $routes->first()['metadata']['groupDescription'],
59+
'item' => $routes->map(\Closure::fromCallable([$this, 'generateEndpointItem']))->toArray(),
8460
];
8561
})->values()->toArray(),
8662
];
8763

64+
65+
if (!empty($this->auth)) {
66+
$collection['auth'] = $this->auth;
67+
}
68+
8869
return json_encode($collection, JSON_PRETTY_PRINT);
8970
}
71+
72+
protected function generateEndpointItem($route)
73+
{
74+
$mode = 'raw';
75+
76+
$method = $route['methods'][0];
77+
return [
78+
'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : $route['uri'],
79+
'request' => [
80+
'url' => $this->makeUrlData($route),
81+
'method' => $method,
82+
'header' => $this->resolveHeadersForRoute($route),
83+
'body' => [
84+
'mode' => $mode,
85+
$mode => json_encode($route['cleanBodyParameters'], JSON_PRETTY_PRINT),
86+
],
87+
'description' => $route['metadata']['description'] ?? null,
88+
'response' => [],
89+
],
90+
];
91+
}
92+
93+
protected function resolveHeadersForRoute($route)
94+
{
95+
$headers = collect($route['headers']);
96+
97+
// Exclude authentication headers if they're handled by Postman auth
98+
$authHeader = $this->getAuthHeader();
99+
if (!empty($authHeader)) {
100+
$headers = $headers->except($authHeader);
101+
}
102+
103+
return $headers
104+
->union([
105+
'Accept' => 'application/json',
106+
])
107+
->map(function ($value, $header) {
108+
return [
109+
'key' => $header,
110+
'value' => $value,
111+
];
112+
})
113+
->values()
114+
->all();
115+
}
116+
117+
protected function makeUrlData($route)
118+
{
119+
[$urlParams, $queryParams] = collect($route['urlParameters'])->partition(function($_, $key) use ($route) {
120+
return Str::contains($route['uri'], '{' . $key . '}');
121+
});
122+
123+
/** @var Collection $queryParams */
124+
$base = [
125+
'protocol' => $this->protocol,
126+
'host' => $this->baseUrl,
127+
// Substitute laravel/symfony query params ({example}) to Postman style, prefixed with a colon
128+
'path' => preg_replace_callback('/\/{(\w+)\??}(?=\/|$)/', function ($matches) {
129+
return '/:' . $matches[1];
130+
}, $route['uri']),
131+
'query' => $queryParams->union($route['queryParameters'])->map(function ($parameter, $key) {
132+
return [
133+
'key' => $key,
134+
'value' => $parameter['value'],
135+
'description' => $parameter['description'],
136+
// Default query params to disabled if they aren't required and have empty values
137+
'disabled' => !$parameter['required'] && empty($parameter['value']),
138+
];
139+
})->values()->toArray(),
140+
];
141+
142+
// If there aren't any url parameters described then return what we've got
143+
/** @var $urlParams Collection */
144+
if ($urlParams->isEmpty()) {
145+
return $base;
146+
}
147+
148+
$base['variable'] = $urlParams->map(function ($parameter, $key) {
149+
return [
150+
'id' => $key,
151+
'key' => $key,
152+
'value' => $parameter['value'],
153+
'description' => $parameter['description'],
154+
];
155+
})->values()->toArray();
156+
157+
return $base;
158+
}
159+
160+
protected function getAuthHeader()
161+
{
162+
$auth = $this->auth;
163+
if (empty($auth) || !is_string($auth['type'] ?? null)) {
164+
return null;
165+
}
166+
167+
switch ($auth['type']) {
168+
case 'bearer':
169+
return 'Authorization';
170+
case 'apikey':
171+
$spec = $auth['apikey'];
172+
173+
if (isset($spec['in']) && $spec['in'] !== 'header') {
174+
return null;
175+
}
176+
177+
return $spec['key'];
178+
default:
179+
return null;
180+
}
181+
}
90182
}

src/Writing/Writer.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,11 @@ protected function writePostmanCollection(Collection $parsedRoutes): void
223223
*/
224224
public function generatePostmanCollection(Collection $routes)
225225
{
226-
$writer = new PostmanCollectionWriter($routes, $this->baseUrl);
226+
/** @var PostmanCollectionWriter $writer */
227+
$writer = app()->makeWith(
228+
PostmanCollectionWriter::class,
229+
['routeGroups' => $routes, 'baseUrl' => $this->baseUrl]
230+
);
227231

228232
return $writer->getCollection();
229233
}

0 commit comments

Comments
 (0)