├── .gitignore ├── .idea ├── encodings.xml ├── laravel-apispec-generator.iml ├── misc.xml ├── modules.xml ├── php-test-framework.xml ├── php.xml └── vcs.xml ├── README.md ├── composer.json ├── composer.lock ├── docker-compose.yml ├── phpunit.xml ├── src ├── ApiSpecOutput.php ├── ApiSpecServiceProvider.php ├── ApiSpecTestCase.php ├── Builders │ ├── AbstractBuilder.php │ ├── BuilderInterface.php │ ├── ToHTTP.php │ └── ToOAS.php ├── Commands │ └── AggregateCommand.php └── config │ └── apispec.php └── test ├── ApiSpecTest.php ├── Builders ├── ToHTTPTest.php ├── ToOASTest.php └── data │ └── ToOASTest │ ├── TestGenerateContent_GET.expected.json │ ├── TestGenerateContent_WithData.expected.json │ ├── testAggregateContent.expected.json │ ├── testAggregateContent.input1.json │ └── testAggregateContent.input2.json └── MockUser.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | nohup.out 3 | ### Laravel template 4 | vendor/ 5 | node_modules/ 6 | npm-debug.log 7 | 8 | # Laravel 4 specific 9 | bootstrap/compiled.php 10 | app/storage/ 11 | 12 | # Laravel 5 & Lumen specific 13 | public/storage 14 | public/hot 15 | storage/*.key 16 | .env.*.php 17 | .env.php 18 | .env 19 | Homestead.yaml 20 | Homestead.json 21 | 22 | # Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer 23 | .rocketeer/ 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 26 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 27 | 28 | # User-specific stuff: 29 | .idea/**/workspace.xml 30 | .idea/**/tasks.xml 31 | .idea/dictionaries 32 | 33 | # Sensitive or high-churn files: 34 | .idea/**/dataSources/ 35 | .idea/**/dataSources.ids 36 | .idea/**/dataSources.xml 37 | .idea/**/dataSources.local.xml 38 | .idea/**/sqlDataSources.xml 39 | .idea/**/dynamic.xml 40 | .idea/**/uiDesigner.xml 41 | 42 | # Gradle: 43 | .idea/**/gradle.xml 44 | .idea/**/libraries 45 | 46 | # CMake 47 | cmake-build-debug/ 48 | 49 | # Mongo Explorer plugin: 50 | .idea/**/mongoSettings.xml 51 | 52 | ## File-based project format: 53 | *.iws 54 | 55 | ## Plugin-specific files: 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/laravel-apispec-generator.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/php-test-framework.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel API Spec Generator 2 | 3 | API Spec generator with Laravel test 4 | 5 | This Package overrides json() 6 | When you use those function, API specs are going to be generated. 7 | 8 | You can select spec below, 9 | 10 | - Rest 11 | - OpenAPI Specification 12 | 13 | ## Usage 14 | 15 | ### Output each specs 16 | 17 | Just use `ApiSpec\ApiSpecTestCase` as base class for API-based test classes. 18 | 19 | ```diff 20 | +use ApiSpec\ApiSpecTestCase; 21 | 22 | class SomeTestCase extends ApiSpecTestCase 23 | { 24 | ``` 25 | 26 | or use trait `ApiSpec\ApiSpecOutput` 27 | 28 | ```diff 29 | +use ApiSpec\ApiSpecOutput; 30 | 31 | class SomeTestCase extends TestCase 32 | { 33 | +use ApiSpecOutput; 34 | //... 35 | } 36 | 37 | ``` 38 | 39 | ### Aggregate output files 40 | 41 | After Output each specs, this command aggregates all specs in one file. 42 | (only supports OAS mode) 43 | 44 | ```bash 45 | php artisan apispec:aggregate 46 | ``` 47 | 48 | ## Configurations 49 | 50 | This package provides config file as `apispec.php` 51 | 52 | ```php 53 | return [ 54 | // Whether to output spec files. 55 | 'isExportSpec' => true, 56 | 57 | // Spec builder class name. You can choose ToOAS or ToHTTP. 58 | 'builder' => \ApiSpec\Builders\ToOAS::class, 59 | ]; 60 | ``` 61 | 62 | ## Output 63 | 64 | ### Rest 65 | 66 | The output format is recognized on several IDE. 67 | 68 | ex) 69 | PHPStorm, IntelliJ IDEA...([2017.3 EAP](https://blog.jetbrains.com/phpstorm/2017/09/phpstorm-2017-3-early-access-program-is-open/) 70 | https://blog.jetbrains.com/phpstorm/2017/09/editor-based-rest-client/ 71 | 72 | ### OAS 73 | 74 | The output format is OpenAPI 3.0.0 75 | 76 | Restrictions are below 77 | 78 | - Security Scheme type supports only JWT 79 | - All request body contents have `required` flag 80 | - Some parameters hard coded. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kotamat/laravel-apispec-generator", 3 | "description": "RestAPI spec generator with Laravel test", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "kotamat", 9 | "email": "kota.matsumoto@scouter.co.jp" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "ApiSpec\\": "src/", 15 | "Test\\ApiSpec\\": "test/" 16 | } 17 | }, 18 | "extra": { 19 | "laravel": { 20 | "providers": [ 21 | "ApiSpec\\ApiSpecServiceProvider" 22 | ] 23 | } 24 | }, 25 | "require": { 26 | "laravel/framework": ">=7.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^8.5", 30 | "mockery/mockery": "^1.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | test: 4 | image: "php:8" 5 | volumes: 6 | - "./:/app" 7 | working_dir: "/app" 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./test 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ApiSpecOutput.php: -------------------------------------------------------------------------------- 1 | __authenticatedUser = $user; 25 | } 26 | 27 | public function json($method, $uri, array $data = [], array $headers = []) 28 | { 29 | $res = parent::json(...func_get_args()); 30 | $route = Route::current(); 31 | $this->outputSpec($uri, $route, $method, $res, $data, $headers); 32 | 33 | return $res; 34 | } 35 | 36 | /** 37 | * output spec file. 38 | * 39 | * @param string $uri request uri 40 | * @param \Illuminate\Routing\Route|null $route request route 41 | * @param string $method method name 42 | * @param TestResponse $response response object 43 | * @param array $data request body 44 | * @param array $headers request headers 45 | * 46 | * @return void 47 | */ 48 | protected function outputSpec( 49 | string $uri, 50 | ?\Illuminate\Routing\Route $route, 51 | string $method, 52 | TestResponse $response, 53 | array $data = [], 54 | array $headers = [], 55 | ) { 56 | if ($this->app->make('config')->get('apispec.isExportSpec')) { 57 | /** @var BuilderInterface $builder */ 58 | $builder = $this->app->make(BuilderInterface::class); 59 | $builder?->setApp($this->app) 60 | ->setMethod($method) 61 | ->setUri($uri) 62 | ->setRoute($route) 63 | ->setData($data) 64 | ->setHeaders(['Content-Type' => 'application/json', 'Accept' => 'application/json']) 65 | ->setHeaders($headers) 66 | ->setResponse($response) 67 | ->setAuthenticatedUser($this->__authenticatedUser) 68 | ->output(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ApiSpecServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 17 | __DIR__ . '/config/apispec.php', 'apispec' 18 | ); 19 | $this->app->bind(BuilderInterface::class, function (Application $app) { 20 | $builderClass = $app->make('config')->get('apispec.builder'); 21 | 22 | return $app->make($builderClass); 23 | }); 24 | } 25 | 26 | public function boot() 27 | { 28 | $this->publishes([ 29 | __DIR__ . '/config/apispec.php' => config_path('apispec.php'), 30 | ]); 31 | if ($this->app->runningInConsole()) { 32 | $this->commands([ 33 | AggregateCommand::class, 34 | ]); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ApiSpecTestCase.php: -------------------------------------------------------------------------------- 1 | app['filesystem']->drive('local')->put($filename, $content); 24 | } 25 | 26 | public function loadOutputs(string $dir, string $pattern): array 27 | { 28 | $allFiles = $this->app['filesystem']->drive('local')->allFiles($dir); 29 | $contents = []; 30 | foreach ($allFiles as $file) { 31 | if (preg_match($pattern, $file)) { 32 | $contents[] = $this->app['filesystem']->drive('local')->get($file); 33 | } 34 | } 35 | return $contents; 36 | } 37 | 38 | /** 39 | * generate apispec content 40 | * 41 | * @return string 42 | */ 43 | abstract public function generateContent(): string; 44 | 45 | ////////////////////// 46 | // setters 47 | ////////////////////// 48 | /** 49 | * @param string $method 50 | * 51 | * @return BuilderInterface 52 | */ 53 | public function setMethod(string $method): BuilderInterface 54 | { 55 | $this->method = $method; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @param string $uri 62 | * 63 | * @return BuilderInterface 64 | */ 65 | public function setUri(string $uri): BuilderInterface 66 | { 67 | $this->uri = $uri; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @param Route $route 74 | * 75 | * @return BuilderInterface 76 | */ 77 | public function setRoute(Route $route): BuilderInterface 78 | { 79 | $this->route=$route; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param array $headers 86 | * 87 | * @return BuilderInterface 88 | */ 89 | public function setHeaders(array $headers): BuilderInterface 90 | { 91 | $this->headers = array_merge($this->headers, $headers); 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * @param TestResponse $response 98 | * 99 | * @return BuilderInterface 100 | */ 101 | public function setResponse(TestResponse $response): BuilderInterface 102 | { 103 | $this->response = $response; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @param array $data 110 | * 111 | * @return BuilderInterface 112 | */ 113 | public function setData(array $data): BuilderInterface 114 | { 115 | $this->data = $data; 116 | 117 | return $this; 118 | } 119 | 120 | public function setAuthenticatedUser($authenticatedUser): BuilderInterface 121 | { 122 | $this->authenticatedUser = $authenticatedUser; 123 | 124 | return $this; 125 | } 126 | 127 | public function setApp(Application $app): BuilderInterface 128 | { 129 | $this->app = $app; 130 | 131 | return $this; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Builders/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | generateContent(); 16 | 17 | $path = preg_replace('/https?:\/\/[0-9\.:a-zA-Z]+\//', '', $this->uri); 18 | $this->saveOutput($path . '/' . $this->method . '.http', $content); 19 | } 20 | 21 | public function generateContent(): string 22 | { 23 | // Uri 24 | $content = "$this->method $this->uri" . PHP_EOL; 25 | 26 | // Header 27 | foreach ($this->headers as $key => $value) { 28 | $content .= "$key: $value" . PHP_EOL; 29 | } 30 | if ($this->authenticatedUser) { 31 | // TODO select token protocol 32 | $content .= "Authorization: Bearer "; 33 | if (method_exists($this->authenticatedUser, 'createToken')) { 34 | $token = $this->authenticatedUser->createToken('test token'); 35 | $content .= $token->accessToken ?? ''; 36 | } 37 | $content .= PHP_EOL; 38 | } 39 | 40 | $content .= PHP_EOL; 41 | 42 | // Content 43 | if (!empty($this->data)) { 44 | $param = \json_encode($this->data, JSON_PRETTY_PRINT); 45 | $content .= $param . PHP_EOL; 46 | } 47 | 48 | // Response 49 | $content .= "# Response:" . PHP_EOL . "#"; 50 | $content .= mb_ereg_replace( 51 | PHP_EOL, 52 | PHP_EOL . '#', 53 | \json_encode($this->response->json(), JSON_PRETTY_PRINT) 54 | ); 55 | 56 | return $content; 57 | } 58 | 59 | public function aggregate(): void 60 | { 61 | // TODO: Implement aggregate() method. 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Builders/ToOAS.php: -------------------------------------------------------------------------------- 1 | generateContent(); 15 | } catch (\Throwable $exception) { 16 | return; 17 | } 18 | 19 | $path = $this->route->uri; 20 | $status = $this->response->status(); 21 | $filename = "$path/$this->method.$status.json"; 22 | $this->saveOutput($filename, $content); 23 | } 24 | 25 | private function getType($data) 26 | { 27 | $type = gettype($data); 28 | if ($type === "array") { 29 | if (array_values($data) === $data) { 30 | $type = "array"; 31 | } else { 32 | $type = "object"; 33 | } 34 | } 35 | 36 | return $type; 37 | } 38 | 39 | public function buildSwaggerObject($data): array 40 | { 41 | $type = $this->getType($data); 42 | switch ($type) { 43 | case "array": 44 | return [ 45 | 'type' => 'array', 46 | 'items' => $this->buildSwaggerObject($data[0]), 47 | ]; 48 | case "object": 49 | $op = [ 50 | 'type' => 'object', 51 | 'properties' => [], 52 | ]; 53 | $keys = array_values(array_filter(array_keys($data), fn($a) => is_string($a))); 54 | if (count($keys) > 0) { 55 | $op['required'] = $keys; 56 | } 57 | foreach ($data as $k => $d) { 58 | if ($this->getType($d) !== "NULL") { 59 | $op['properties'][$k] = $this->buildSwaggerObject($d); 60 | } 61 | } 62 | if (empty($op['properties'])) { 63 | $op['properties'] = new \stdClass(); 64 | } 65 | 66 | return $op; 67 | default: 68 | return [ 69 | 'type' => $type, 70 | 'example' => $data, 71 | ]; 72 | } 73 | } 74 | 75 | public function generateContent(): string 76 | { 77 | $symfonyRequest = SymfonyRequest::create($this->uri); 78 | $path = "/" . $this->route->uri; 79 | $content = [ 80 | 'openapi' => '3.0.0', 81 | 'info' => [ 82 | 'title' => "auto generated spec", 83 | 'version' => "0.0.0", 84 | ], 85 | 'paths' => [ 86 | $path => [ 87 | strtolower($this->method) => [ 88 | "summary" => $path, 89 | "description" => $path, 90 | "operationId" => "$path:$this->method", 91 | "security" => $this->authenticatedUser ? [ 92 | [ 93 | "bearerAuth" => [], 94 | ], 95 | ] : [], 96 | "responses" => [ 97 | $this->response->status() => [ 98 | "description" => "", 99 | ], 100 | ], 101 | ], 102 | ], 103 | ], 104 | ]; 105 | if (!empty($symfonyRequest->query->all())) { 106 | if (empty($content['paths'][$path][strtolower($this->method)]["parameters"])) { 107 | $content['paths'][$path][strtolower($this->method)]["parameters"] = []; 108 | } 109 | foreach ($symfonyRequest->query->all() as $key => $value) { 110 | $content['paths'][$path][strtolower($this->method)]["parameters"][] = [ 111 | "in" => "query", 112 | "name" => $key, 113 | "schema" => [ 114 | "type" => $this->getType($value), 115 | ], 116 | "description" => "$value", 117 | ]; 118 | } 119 | } 120 | if ($this->headers) { 121 | if (empty($content['paths'][$path][strtolower($this->method)]["parameters"])) { 122 | $content['paths'][$path][strtolower($this->method)]["parameters"] = []; 123 | } 124 | foreach ($this->headers as $key => $value) { 125 | $content['paths'][$path][strtolower($this->method)]["parameters"][] = [ 126 | "in" => "header", 127 | "name" => $key, 128 | "schema" => [ 129 | "type" => $this->getType($value), 130 | ], 131 | "description" => "$value", 132 | ]; 133 | } 134 | } 135 | if ($this->route->parameters) { 136 | if (empty($content['paths'][$path][strtolower($this->method)]["parameters"])) { 137 | $content['paths'][$path][strtolower($this->method)]["parameters"] = []; 138 | } 139 | foreach ($this->route->parameters as $key => $parameter) { 140 | $param = $parameter; 141 | if ($parameter instanceof Model) { 142 | $param = $parameter->getKey(); 143 | } 144 | $content['paths'][$path][strtolower($this->method)]["parameters"][] = [ 145 | "in" => "path", 146 | "name" => $key, 147 | "required" => true, 148 | "schema" => [ 149 | "type" => $this->getType($param), 150 | ], 151 | "description" => "$param", 152 | ]; 153 | } 154 | } 155 | if ($this->response->content() && !empty($this->response->json())) { 156 | $response = $this->response->json(); 157 | if (is_array($response)) { 158 | $response = $this->buildSwaggerObject($response); 159 | } 160 | 161 | $response['title'] = $path . '_' . $this->method . '_response_' . $this->response->status(); 162 | 163 | $content['paths'][$path][strtolower($this->method)]['responses'][$this->response->status()]["content"] = [ 164 | "application/json" => [ 165 | "schema" => $response, 166 | ], 167 | ]; 168 | } 169 | if ($this->data) { 170 | $requestBody = $this->buildSwaggerObject($this->data); 171 | 172 | $requestBody['title'] = $path . '_' . $this->method . '_request'; 173 | 174 | $content['paths'][$path][strtolower($this->method)]['requestBody'] = [ 175 | "content" => [ 176 | "application/json" => [ 177 | "schema" => $requestBody, 178 | ], 179 | ], 180 | ]; 181 | } 182 | if ($this->authenticatedUser) { 183 | $content['components'] = [ 184 | "securitySchemes" => [ 185 | "bearerAuth" => [ 186 | "type" => "http", 187 | "scheme" => "bearer", 188 | "bearerFormat" => "JWT", 189 | ], 190 | ], 191 | ]; 192 | } 193 | 194 | return json_encode($content, JSON_PRETTY_PRINT); 195 | } 196 | 197 | public function aggregate(): void 198 | { 199 | $contents = $this->loadOutputs('api', '/.*\.json/'); 200 | 201 | $aggregated = $this->aggregateContent($contents); 202 | $this->saveOutput('all.json', $aggregated); 203 | } 204 | 205 | /** 206 | * @param array $contents 207 | * 208 | * @return string 209 | */ 210 | public function aggregateContent(array $contents): string 211 | { 212 | $aggregated = []; 213 | foreach ($contents as $contentStr) { 214 | $content = json_decode($contentStr, true); 215 | if (empty($aggregated)) { 216 | $aggregated = $content; 217 | } else { 218 | $path = array_key_first($content['paths']); 219 | if (empty($aggregated['paths'][$path])) { 220 | $aggregated['paths'][$path] = $content['paths'][$path]; 221 | } else { 222 | $method = array_key_first($content['paths'][$path]); 223 | if (empty($aggregated['paths'][$path][$method])) { 224 | $aggregated['paths'][$path][$method] = $content['paths'][$path][$method]; 225 | } else { 226 | $status = 227 | array_key_first($content['paths'][$path][$method]['responses']); 228 | $aggregated['paths'][$path][$method]['responses'][$status] = 229 | $content['paths'][$path][$method]['responses'][$status]; 230 | } 231 | } 232 | if (empty($aggregated['components']) && !empty($content['components'])) { 233 | $aggregated['components'] = $content['components'] ?? []; 234 | } 235 | } 236 | } 237 | 238 | return json_encode($aggregated); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Commands/AggregateCommand.php: -------------------------------------------------------------------------------- 1 | builder = app()->make(BuilderInterface::class); 42 | } 43 | 44 | /** 45 | * Execute the console command. 46 | * 47 | * @return void 48 | */ 49 | public function handle(): void 50 | { 51 | $this->builder->setApp(app())->aggregate(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config/apispec.php: -------------------------------------------------------------------------------- 1 | true, 6 | 'builder' => \ApiSpec\Builders\ToOAS::class, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/ApiSpecTest.php: -------------------------------------------------------------------------------- 1 | setMethod('GET') 19 | ->setUri('http://hoge.com/user/1') 20 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 21 | ->generateContent(); 22 | 23 | $expected = <<< EOS 24 | GET http://hoge.com/user/1 25 | 26 | # Response: 27 | #{ 28 | # "name": "huga" 29 | #} 30 | EOS; 31 | 32 | $this->assertEquals($expected, $content); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function TestGenerateContent_WithData() 39 | { 40 | $content = (new ToHTTP())->setMethod('POST') 41 | ->setUri('http://hoge.com/user/') 42 | ->setData(['name' => 'hoge']) 43 | ->setHeaders(['Accept' => 'application/json']) 44 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 45 | ->setAuthenticatedUser(new MockUser()) 46 | ->generateContent(); 47 | 48 | $expected = <<< EOS 49 | POST http://hoge.com/user/ 50 | Accept: application/json 51 | Authorization: Bearer token 52 | 53 | { 54 | "name": "hoge" 55 | } 56 | # Response: 57 | #{ 58 | # "name": "huga" 59 | #} 60 | EOS; 61 | 62 | $this->assertEquals($expected, $content); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Builders/ToHTTPTest.php: -------------------------------------------------------------------------------- 1 | setMethod('GET') 20 | ->setUri('http://hoge.com/user/1') 21 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 22 | ->generateContent(); 23 | 24 | $expected = <<< EOS 25 | GET http://hoge.com/user/1 26 | 27 | # Response: 28 | #{ 29 | # "name": "huga" 30 | #} 31 | EOS; 32 | 33 | $this->assertEquals($expected, $content); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function TestGenerateContent_WithData() 40 | { 41 | $content = (new ToHTTP())->setMethod('POST') 42 | ->setUri('http://hoge.com/user/') 43 | ->setData(['name' => 'hoge']) 44 | ->setHeaders(['Accept' => 'application/json']) 45 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 46 | ->setAuthenticatedUser(new MockUser()) 47 | ->generateContent(); 48 | 49 | $expected = <<< EOS 50 | POST http://hoge.com/user/ 51 | Accept: application/json 52 | Authorization: Bearer token 53 | 54 | { 55 | "name": "hoge" 56 | } 57 | # Response: 58 | #{ 59 | # "name": "huga" 60 | #} 61 | EOS; 62 | 63 | $this->assertEquals($expected, $content); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/Builders/ToOASTest.php: -------------------------------------------------------------------------------- 1 | setMethod('GET') 22 | ->setRoute($route) 23 | ->setUri("http://localhost/user/1?hoge=aaa&fuga=bbb") 24 | ->setHeaders(["X-User-Id" => 1]) 25 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 26 | ->generateContent(); 27 | 28 | $expected = file_get_contents(__DIR__ . "/data/ToOASTest/" . __FUNCTION__ . ".expected.json"); 29 | 30 | $this->assertEquals(json_decode($expected), json_decode($content)); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function TestGenerateContent_WithData() 37 | { 38 | $route = new Route("POST", "/user/", []); 39 | $content = (new ToOAS())->setMethod('POST') 40 | ->setRoute($route) 41 | ->setData(['name' => 'hoge']) 42 | ->setHeaders(['Accept' => 'application/json']) 43 | ->setResponse(new TestResponse(new Response(['name' => 'huga']))) 44 | ->setAuthenticatedUser(new MockUser()) 45 | ->generateContent(); 46 | 47 | $expected = file_get_contents(__DIR__ . "/data/ToOASTest/" . __FUNCTION__ . ".expected.json"); 48 | 49 | $this->assertEquals(json_decode($expected), json_decode($content)); 50 | } 51 | 52 | public function testAggregateContent() 53 | { 54 | $builder = new ToOAS(); 55 | 56 | $input = [ 57 | file_get_contents(__DIR__ . "/data/ToOASTest/" . __FUNCTION__ . ".input1.json"), 58 | file_get_contents(__DIR__ . "/data/ToOASTest/" . __FUNCTION__ . ".input2.json"), 59 | ]; 60 | $content = $builder->aggregateContent($input); 61 | 62 | $expected = file_get_contents(__DIR__ . "/data/ToOASTest/" . __FUNCTION__ . ".expected.json"); 63 | 64 | $this->assertEquals(json_decode($expected), json_decode($content)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/Builders/data/ToOASTest/TestGenerateContent_GET.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "auto generated spec", 5 | "version": "0.0.0" 6 | }, 7 | "paths": { 8 | "\/user\/1": { 9 | "get": { 10 | "summary": "\/user\/1", 11 | "description": "\/user\/1", 12 | "operationId": "\/user\/1:GET", 13 | "security": [], 14 | "responses": { 15 | "200": { 16 | "description": "", 17 | "content": { 18 | "application\/json": { 19 | "schema": { 20 | "title": "/user/1_GET_response_200", 21 | "type": "object", 22 | "properties": { 23 | "name": { 24 | "type": "string", 25 | "example": "huga" 26 | } 27 | }, 28 | "required": [ 29 | "name" 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "parameters": [ 37 | { 38 | "in": "query", 39 | "name": "hoge", 40 | "schema": { 41 | "type": "string" 42 | }, 43 | "description": "aaa" 44 | }, 45 | { 46 | "in": "query", 47 | "name": "fuga", 48 | "schema": { 49 | "type": "string" 50 | }, 51 | "description": "bbb" 52 | }, 53 | { 54 | "in": "header", 55 | "name": "X-User-Id", 56 | "schema": { 57 | "type": "integer" 58 | }, 59 | "description": "1" 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /test/Builders/data/ToOASTest/TestGenerateContent_WithData.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "auto generated spec", 5 | "version": "0.0.0" 6 | }, 7 | "paths": { 8 | "\/user": { 9 | "post": { 10 | "summary": "\/user", 11 | "description": "\/user", 12 | "operationId": "\/user:POST", 13 | "security": [ 14 | { 15 | "bearerAuth": [] 16 | } 17 | ], 18 | "responses": { 19 | "200": { 20 | "description": "", 21 | "content": { 22 | "application\/json": { 23 | "schema": { 24 | "title": "/user_POST_response_200", 25 | "type": "object", 26 | "properties": { 27 | "name": { 28 | "type": "string", 29 | "example": "huga" 30 | } 31 | }, 32 | "required": [ 33 | "name" 34 | ] 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "parameters": [ 41 | { 42 | "in": "header", 43 | "name": "Accept", 44 | "schema": { 45 | "type": "string" 46 | }, 47 | "description": "application\/json" 48 | } 49 | ], 50 | "requestBody": { 51 | "content": { 52 | "application\/json": { 53 | "schema": { 54 | "title": "/user_POST_request", 55 | "type": "object", 56 | "properties": { 57 | "name": { 58 | "type": "string", 59 | "example": "hoge" 60 | } 61 | }, 62 | "required": [ 63 | "name" 64 | ] 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }, 72 | "components": { 73 | "securitySchemes": { 74 | "bearerAuth": { 75 | "type": "http", 76 | "scheme": "bearer", 77 | "bearerFormat": "JWT" 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /test/Builders/data/ToOASTest/testAggregateContent.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "auto generated spec", 5 | "version": "0.0.0" 6 | }, 7 | "paths": { 8 | "\/user\/1": { 9 | "get": { 10 | "summary": "\/user\/1", 11 | "description": "\/user\/1", 12 | "operationId": "\/user\/1", 13 | "security": [], 14 | "responses": { 15 | "200": { 16 | "description": "", 17 | "content": { 18 | "application\/json": { 19 | "schema": { 20 | "required": [ 21 | "name" 22 | ], 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "example": "huga" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "post": { 36 | "summary": "\/user\/1", 37 | "description": "\/user\/1", 38 | "operationId": "\/user\/1", 39 | "security": [], 40 | "responses": { 41 | "200": { 42 | "description": "", 43 | "content": { 44 | "application\/json": { 45 | "schema": { 46 | "required": [ 47 | "name" 48 | ], 49 | "properties": { 50 | "name": { 51 | "type": "string", 52 | "example": "huga" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /test/Builders/data/ToOASTest/testAggregateContent.input1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "auto generated spec", 5 | "version": "0.0.0" 6 | }, 7 | "paths": { 8 | "\/user\/1": { 9 | "get": { 10 | "summary": "\/user\/1", 11 | "description": "\/user\/1", 12 | "operationId": "\/user\/1", 13 | "security": [], 14 | "responses": { 15 | "200": { 16 | "description": "", 17 | "content": { 18 | "application\/json": { 19 | "schema": { 20 | "required": [ 21 | "name" 22 | ], 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "example": "huga" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/Builders/data/ToOASTest/testAggregateContent.input2.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "auto generated spec", 5 | "version": "0.0.0" 6 | }, 7 | "paths": { 8 | "\/user\/1": { 9 | "post": { 10 | "summary": "\/user\/1", 11 | "description": "\/user\/1", 12 | "operationId": "\/user\/1", 13 | "security": [], 14 | "responses": { 15 | "200": { 16 | "description": "", 17 | "content": { 18 | "application\/json": { 19 | "schema": { 20 | "required": [ 21 | "name" 22 | ], 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "example": "huga" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/MockUser.php: -------------------------------------------------------------------------------- 1 | 'token', 12 | ]; 13 | } 14 | } 15 | --------------------------------------------------------------------------------