PostmanCollectionWriter.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. namespace Knuckles\Scribe\Writing;
  3. use Illuminate\Support\Collection;
  4. use Illuminate\Support\Facades\URL;
  5. use Illuminate\Support\Str;
  6. use Ramsey\Uuid\Uuid;
  7. use ReflectionMethod;
  8. class PostmanCollectionWriter
  9. {
  10. /**
  11. * @var Collection
  12. */
  13. private $routeGroups;
  14. /**
  15. * @var string
  16. */
  17. private $baseUrl;
  18. /**
  19. * @var string
  20. */
  21. private $protocol;
  22. /**
  23. * @var array|null
  24. */
  25. private $auth;
  26. /**
  27. * CollectionWriter constructor.
  28. *
  29. * @param Collection $routeGroups
  30. */
  31. public function __construct(Collection $routeGroups, $baseUrl)
  32. {
  33. $this->routeGroups = $routeGroups;
  34. $this->protocol = Str::startsWith($baseUrl, 'https') ? 'https' : 'http';
  35. $this->baseUrl = $this->getBaseUrl($baseUrl);
  36. $this->auth = config('scribe.postman.auth');
  37. }
  38. public function makePostmanCollection()
  39. {
  40. $collection = [
  41. 'variables' => [],
  42. 'info' => [
  43. 'name' => config('scribe.title') ?: config('app.name') . ' API',
  44. '_postman_id' => Uuid::uuid4()->toString(),
  45. 'description' => config('scribe.postman.description') ?: '',
  46. 'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
  47. ],
  48. 'item' => $this->routeGroups->map(function (Collection $routes, $groupName) {
  49. return [
  50. 'name' => $groupName,
  51. 'description' => $routes->first()['metadata']['groupDescription'],
  52. 'item' => $routes->map(\Closure::fromCallable([$this, 'generateEndpointItem']))->toArray(),
  53. ];
  54. })->values()->toArray(),
  55. ];
  56. if (! empty($this->auth)) {
  57. $collection['auth'] = $this->auth;
  58. }
  59. return json_encode($collection, JSON_PRETTY_PRINT);
  60. }
  61. protected function generateEndpointItem($route)
  62. {
  63. $method = $route['methods'][0];
  64. return [
  65. 'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : $route['uri'],
  66. 'request' => [
  67. 'url' => $this->makeUrlData($route),
  68. 'method' => $method,
  69. 'header' => $this->resolveHeadersForRoute($route),
  70. 'body' => $this->getBodyOptions($route),
  71. 'description' => $route['metadata']['description'] ?? null,
  72. 'response' => [],
  73. ],
  74. ];
  75. }
  76. protected function getBodyOptions($route)
  77. {
  78. $body = [];
  79. $contentType = $route['headers']['Content-Type'];
  80. switch ($contentType) {
  81. case 'multipart/form-data':
  82. $mode = 'formdata';
  83. break;
  84. case 'application/json':
  85. case 'default':
  86. $mode = 'raw';
  87. }
  88. $body['mode'] = $mode;
  89. switch ($mode) {
  90. case 'formdata':
  91. foreach ($route['cleanBodyParameters'] as $key => $value) {
  92. $params = [
  93. 'key' => $key,
  94. 'value' => $value,
  95. 'type' => 'text'
  96. ];
  97. $body[$mode][] = $params;
  98. }
  99. foreach ($route['fileParameters'] as $key => $value) {
  100. $params = [
  101. 'key' => $key,
  102. 'src' => [],
  103. 'type' => 'file'
  104. ];
  105. $body[$mode][] = $params;
  106. }
  107. break;
  108. case 'raw':
  109. case 'default':
  110. $body[$mode] = json_encode($route['cleanBodyParameters'], JSON_PRETTY_PRINT);
  111. $body['options'][$mode]['language'] = 'json';
  112. }
  113. return $body;
  114. }
  115. protected function resolveHeadersForRoute($route)
  116. {
  117. $headers = collect($route['headers']);
  118. // Exclude authentication headers if they're handled by Postman auth
  119. $authHeader = $this->getAuthHeader();
  120. if (! empty($authHeader)) {
  121. $headers = $headers->except($authHeader);
  122. }
  123. return $headers
  124. ->union([
  125. 'Accept' => 'application/json',
  126. ])
  127. ->map(function ($value, $header) {
  128. // Allow users to write ['header' => '@{{value}}'] in config
  129. // and have it rendered properly as {{value}} in the Postman collection.
  130. $value = str_replace('@{{', '{{', $value);
  131. return [
  132. 'key' => $header,
  133. 'value' => $value,
  134. ];
  135. })
  136. ->values()
  137. ->all();
  138. }
  139. protected function makeUrlData($route)
  140. {
  141. // URL Parameters are collected by the `UrlParameters` strategies, but only make sense if they're in the route
  142. // definition. Filter out any URL parameters that don't appear in the URL.
  143. $urlParams = collect($route['urlParameters'])->filter(function ($_, $key) use ($route) {
  144. return Str::contains($route['uri'], '{' . $key . '}');
  145. });
  146. $base = [
  147. 'protocol' => $this->protocol,
  148. 'host' => $this->baseUrl,
  149. // Substitute laravel/symfony query params ({example}) to Postman style, prefixed with a colon
  150. 'path' => preg_replace_callback('/\/{(\w+)\??}(?=\/|$)/', function ($matches) {
  151. return '/:' . $matches[1];
  152. }, $route['uri']),
  153. 'query' => collect($route['queryParameters'] ?? [])->map(function ($parameterData, $key) {
  154. $key = rtrim($key,".*");
  155. return [
  156. 'key' => $key,
  157. 'value' => urlencode($parameterData['value']),
  158. 'description' => strip_tags($parameterData['description']),
  159. // Default query params to disabled if they aren't required and have empty values
  160. 'disabled' => !($parameterData['required'] ?? false) && empty($parameterData['value']),
  161. ];
  162. })->values()->toArray(),
  163. ];
  164. // If there aren't any url parameters described then return what we've got
  165. /** @var $urlParams Collection */
  166. if ($urlParams->isEmpty()) {
  167. return $base;
  168. }
  169. $base['variable'] = $urlParams->map(function ($parameter, $key) {
  170. return [
  171. 'id' => $key,
  172. 'key' => $key,
  173. 'value' => urlencode($parameter['value']),
  174. 'description' => $parameter['description'],
  175. ];
  176. })->values()->toArray();
  177. return $base;
  178. }
  179. protected function getAuthHeader()
  180. {
  181. $auth = $this->auth;
  182. if (empty($auth) || ! is_string($auth['type'] ?? null)) {
  183. return null;
  184. }
  185. switch ($auth['type']) {
  186. case 'bearer':
  187. return 'Authorization';
  188. case 'apikey':
  189. $spec = $auth['apikey'];
  190. if (isset($spec['in']) && $spec['in'] !== 'header') {
  191. return null;
  192. }
  193. return $spec['key'];
  194. default:
  195. return null;
  196. }
  197. }
  198. protected function getBaseUrl($baseUrl)
  199. {
  200. if (Str::contains(app()->version(), 'Lumen')) { //Is Lumen
  201. $reflectionMethod = new ReflectionMethod(\Laravel\Lumen\Routing\UrlGenerator::class, 'getRootUrl');
  202. $reflectionMethod->setAccessible(true);
  203. $url = app('url');
  204. return $reflectionMethod->invokeArgs($url, ['', $baseUrl]);
  205. }
  206. return URL::formatRoot('', $baseUrl);
  207. }
  208. }