Writer.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <?php
  2. namespace Knuckles\Scribe\Writing;
  3. use Illuminate\Support\Facades\Storage;
  4. use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
  5. use Knuckles\Scribe\Tools\DocumentationConfig;
  6. use Knuckles\Scribe\Tools\Globals;
  7. use Knuckles\Scribe\Tools\Utils;
  8. use Symfony\Component\Yaml\Yaml;
  9. class Writer
  10. {
  11. private DocumentationConfig $config;
  12. private bool $isStatic;
  13. private string $markdownOutputPath;
  14. private string $staticTypeOutputPath;
  15. private string $laravelTypeOutputPath;
  16. protected array $generatedFiles = [
  17. 'postman' => null,
  18. 'openapi' => null,
  19. 'html' => null,
  20. 'blade' => null,
  21. 'assets' => [
  22. 'js' => null,
  23. 'css' => null,
  24. 'images' => null,
  25. ],
  26. ];
  27. private string $laravelAssetsPath;
  28. public function __construct(DocumentationConfig $config = null, $docsName = 'scribe')
  29. {
  30. $this->markdownOutputPath = ".{$docsName}"; //.scribe by default
  31. $this->laravelTypeOutputPath = "resources/views/$docsName";
  32. // If no config is injected, pull from global. Makes testing easier.
  33. $this->config = $config ?: new DocumentationConfig(config($docsName));
  34. $this->isStatic = $this->config->get('type') === 'static';
  35. $this->staticTypeOutputPath = rtrim($this->config->get('static.output_path', 'public/docs'), '/');
  36. $this->laravelAssetsPath = $this->config->get('laravel.assets_directory')
  37. ? '/' . $this->config->get('laravel.assets_directory')
  38. : '/vendor/scribe';
  39. }
  40. /**
  41. * @param array[] $groupedEndpoints
  42. */
  43. public function writeDocs(array $groupedEndpoints)
  44. {
  45. // The static assets (js/, css/, and images/) always go in public/docs/.
  46. // For 'static' docs, the output files (index.html, collection.json) go in public/docs/.
  47. // For 'laravel' docs, the output files (index.blade.php, collection.json)
  48. // go in resources/views/scribe/ and storage/app/scribe/ respectively.
  49. $this->writeHtmlDocs($groupedEndpoints);
  50. $this->writePostmanCollection($groupedEndpoints);
  51. $this->writeOpenAPISpec($groupedEndpoints);
  52. $this->runAfterGeneratingHook();
  53. }
  54. protected function writePostmanCollection(array $groups): void
  55. {
  56. if ($this->config->get('postman.enabled', true)) {
  57. c::info('Generating Postman collection');
  58. $collection = $this->generatePostmanCollection($groups);
  59. if ($this->isStatic) {
  60. $collectionPath = "{$this->staticTypeOutputPath}/collection.json";
  61. file_put_contents($collectionPath, $collection);
  62. } else {
  63. Storage::disk('local')->put('scribe/collection.json', $collection);
  64. $collectionPath = 'storage/app/scribe/collection.json';
  65. }
  66. c::success("Wrote Postman collection to: {$collectionPath}");
  67. $this->generatedFiles['postman'] = realpath($collectionPath);
  68. }
  69. }
  70. protected function writeOpenAPISpec(array $parsedRoutes): void
  71. {
  72. if ($this->config->get('openapi.enabled', false)) {
  73. c::info('Generating OpenAPI specification');
  74. $spec = $this->generateOpenAPISpec($parsedRoutes);
  75. if ($this->isStatic) {
  76. $specPath = "{$this->staticTypeOutputPath}/openapi.yaml";
  77. file_put_contents($specPath, $spec);
  78. } else {
  79. Storage::disk('local')->put('scribe/openapi.yaml', $spec);
  80. $specPath = 'storage/app/scribe/openapi.yaml';
  81. }
  82. c::success("Wrote OpenAPI specification to: {$specPath}");
  83. $this->generatedFiles['openapi'] = realpath($specPath);
  84. }
  85. }
  86. /**
  87. * Generate Postman collection JSON file.
  88. *
  89. * @param array[] $groupedEndpoints
  90. *
  91. * @return string
  92. */
  93. public function generatePostmanCollection(array $groupedEndpoints): string
  94. {
  95. /** @var PostmanCollectionWriter $writer */
  96. $writer = app()->makeWith(PostmanCollectionWriter::class, ['config' => $this->config]);
  97. $collection = $writer->generatePostmanCollection($groupedEndpoints);
  98. $overrides = $this->config->get('postman.overrides', []);
  99. if (count($overrides)) {
  100. foreach ($overrides as $key => $value) {
  101. data_set($collection, $key, $value);
  102. }
  103. }
  104. return json_encode($collection, JSON_PRETTY_PRINT);
  105. }
  106. /**
  107. * @param array[] $groupedEndpoints
  108. *
  109. * @return string
  110. */
  111. public function generateOpenAPISpec(array $groupedEndpoints): string
  112. {
  113. /** @var OpenAPISpecWriter $writer */
  114. $writer = app()->makeWith(OpenAPISpecWriter::class, ['config' => $this->config]);
  115. $spec = $writer->generateSpecContent($groupedEndpoints);
  116. $overrides = $this->config->get('openapi.overrides', []);
  117. if (count($overrides)) {
  118. foreach ($overrides as $key => $value) {
  119. data_set($spec, $key, $value);
  120. }
  121. }
  122. return Yaml::dump($spec, 20, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
  123. }
  124. protected function performFinalTasksForLaravelType(): void
  125. {
  126. if (!is_dir($this->laravelTypeOutputPath)) {
  127. mkdir($this->laravelTypeOutputPath, 0777, true);
  128. }
  129. $publicDirectory = app()->get('path.public');
  130. if (!is_dir($publicDirectory . $this->laravelAssetsPath)) {
  131. mkdir($publicDirectory . $this->laravelAssetsPath, 0777, true);
  132. }
  133. // Transform output HTML to a Blade view
  134. rename("{$this->staticTypeOutputPath}/index.html", "$this->laravelTypeOutputPath/index.blade.php");
  135. // Move assets from public/docs to public/vendor/scribe or config('laravel.assets_directory')
  136. // We need to do this delete first, otherwise move won't work if folder exists
  137. Utils::deleteDirectoryAndContents($publicDirectory . $this->laravelAssetsPath);
  138. rename("{$this->staticTypeOutputPath}/", $publicDirectory . $this->laravelAssetsPath);
  139. $contents = file_get_contents("$this->laravelTypeOutputPath/index.blade.php");
  140. // Rewrite asset links to go through Laravel
  141. $contents = preg_replace('#href="\.\./docs/css/(.+?)"#', 'href="{{ asset("' . $this->laravelAssetsPath . '/css/$1") }}"', $contents);
  142. $contents = preg_replace('#src="\.\./docs/(js|images)/(.+?)"#', 'src="{{ asset("' . $this->laravelAssetsPath . '/$1/$2") }}"', $contents);
  143. $contents = str_replace('href="../docs/collection.json"', 'href="{{ route("scribe.postman") }}"', $contents);
  144. $contents = str_replace('href="../docs/openapi.yaml"', 'href="{{ route("scribe.openapi") }}"', $contents);
  145. file_put_contents("$this->laravelTypeOutputPath/index.blade.php", $contents);
  146. }
  147. public function writeHtmlDocs(array $groupedEndpoints): void
  148. {
  149. c::info('Writing ' . ($this->isStatic ? 'HTML' : 'Blade') . ' docs...');
  150. // Then we convert them to HTML, and throw in the endpoints as well.
  151. /** @var HtmlWriter $writer */
  152. $writer = app()->makeWith(HtmlWriter::class, ['config' => $this->config]);
  153. $writer->generate($groupedEndpoints, $this->markdownOutputPath, $this->staticTypeOutputPath);
  154. if (!$this->isStatic) {
  155. $this->performFinalTasksForLaravelType();
  156. }
  157. if ($this->isStatic) {
  158. $outputPath = rtrim($this->staticTypeOutputPath, '/') . '/';
  159. c::success("Wrote HTML docs and assets to: $outputPath");
  160. $this->generatedFiles['html'] = realpath("{$outputPath}index.html");
  161. $assetsOutputPath = $outputPath;
  162. } else {
  163. $outputPath = rtrim($this->laravelTypeOutputPath, '/') . '/';
  164. c::success("Wrote Blade docs to: $outputPath");
  165. $this->generatedFiles['blade'] = realpath("{$outputPath}index.blade.php");
  166. $assetsOutputPath = app()->get('path.public') . $this->laravelAssetsPath;
  167. c::success("Wrote Laravel assets to: " . realpath($assetsOutputPath));
  168. }
  169. $this->generatedFiles['assets']['js'] = realpath("{$assetsOutputPath}js");
  170. $this->generatedFiles['assets']['css'] = realpath("{$assetsOutputPath}css");
  171. $this->generatedFiles['assets']['images'] = realpath("{$assetsOutputPath}images");
  172. }
  173. protected function runAfterGeneratingHook()
  174. {
  175. if (is_callable(Globals::$__afterGenerating)) {
  176. c::info("Running `afterGenerating()` hook...");
  177. call_user_func_array(Globals::$__afterGenerating, [$this->generatedFiles]);
  178. }
  179. }
  180. }