├── .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 |
165 |
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 |
--------------------------------------------------------------------------------