swagger_autogenerate 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +277 -89
- data/lib/swagger_autogenerate/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82b7c6523f9a6f361c4f78be66068aab528b9707419a11ae61b5264878706ef1
|
|
4
|
+
data.tar.gz: f7671a245cb3a4a89452db82f6d5ba48a1f5b810c1a52d2323cee1a94f76726a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e4b21d768fcbd45e6a082f3caf0c437079d535d53c5fa9466aa1c7b70c69e289de2fadc9d440d2c9128cfbcf99dabeb1cf1f5742154ede03d203cc86c3a8cac
|
|
7
|
+
data.tar.gz: 4e0b9ce2a3cc3d9dae32383b8980d4b31e70b0f7611c8e3769e0ef7360da94e42ef060aa62860366698fafdb8bd0dbdf3b2bc475e4ad6d257b5f41a2c202c8eb
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -2,27 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Generate **OpenAPI / Swagger YAML** from your existing Rails RSpec request (or controller) specs.
|
|
4
4
|
|
|
5
|
-
Designed to drop into projects that
|
|
5
|
+
Designed to drop into projects that use **`rswag-api`** and **`rswag-ui`**:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- No hand-maintained path definitions for happy-path coverage
|
|
11
|
-
- Works with any Rails app; rswag-friendly defaults out of the box
|
|
7
|
+
1. Run specs with `SWAGGER_GENERATE=1` → one YAML fragment per resource under `swagger/v1/`
|
|
8
|
+
2. Combine fragments into `swagger/v1/swagger.yaml`
|
|
9
|
+
3. Rswag serves that file at `/api-docs`
|
|
12
10
|
|
|
13
11
|
## Dependencies
|
|
14
12
|
|
|
15
13
|
- Ruby `>= 2.7`
|
|
16
14
|
- Rails `>= 5.2`
|
|
17
15
|
- [rspec-rails](https://github.com/rspec/rspec-rails) in the host app
|
|
18
|
-
-
|
|
16
|
+
- Recommended: [`rswag-api`](https://github.com/rswag/rswag) + [`rswag-ui`](https://github.com/rswag/rswag)
|
|
19
17
|
|
|
20
18
|
## Installation
|
|
21
19
|
|
|
22
|
-
Add to the test group:
|
|
23
|
-
|
|
24
20
|
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
gem 'rswag-api'
|
|
23
|
+
gem 'rswag-ui'
|
|
24
|
+
|
|
25
25
|
group :test do
|
|
26
|
+
gem 'rspec-rails'
|
|
26
27
|
gem 'swagger_autogenerate'
|
|
27
28
|
end
|
|
28
29
|
```
|
|
@@ -31,57 +32,278 @@ end
|
|
|
31
32
|
bundle install
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Then add the host-app files below (copy-paste and adjust titles / URLs for your API).
|
|
36
|
+
|
|
37
|
+
---
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
## Host app setup (required for rswag UI)
|
|
40
|
+
|
|
41
|
+
### 1. Mount rswag in `config/routes.rb`
|
|
37
42
|
|
|
38
43
|
```ruby
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
mount Rswag::Ui::Engine => "/api-docs"
|
|
45
|
+
mount Rswag::Api::Engine => "/api-docs"
|
|
46
|
+
```
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
### 2. `config/initializers/rswag_api.rb`
|
|
49
|
+
|
|
50
|
+
Serves `swagger/v1/swagger.yaml` at `/api-docs/v1/swagger.yaml`.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# frozen_string_literal: true
|
|
54
|
+
|
|
55
|
+
Rswag::Api.configure do |c|
|
|
56
|
+
# Serves files from swagger/ — so swagger/v1/swagger.yaml → /api-docs/v1/swagger.yaml
|
|
57
|
+
c.openapi_root = Rails.root.join("swagger").to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Rswag::Ui.configure do |c|
|
|
61
|
+
c.openapi_endpoint "/api-docs/v1/swagger.yaml", "My API V1"
|
|
45
62
|
end
|
|
46
63
|
```
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
### 3. `config/initializers/swagger_autogenerate.rb`
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
Guard with `defined?` so `rails s` does not crash when the gem is only in the `:test` group.
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
```ruby
|
|
70
|
+
# frozen_string_literal: true
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
# swagger_autogenerate is only loaded in the test (and optionally development) group.
|
|
73
|
+
# Guard so `rails s` in development does not crash when the gem is not required.
|
|
74
|
+
return unless defined?(SwaggerAutogenerate)
|
|
75
|
+
|
|
76
|
+
SwaggerAutogenerate.configure do |config|
|
|
77
|
+
# Where SWAGGER_GENERATE writes files (rswag default layout)
|
|
78
|
+
config.default_path = "swagger/v1"
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
# OpenAPI info
|
|
81
|
+
config.info_title = "My API"
|
|
82
|
+
config.info_description = "Public HTTP API"
|
|
83
|
+
config.info_version = "1.0.0"
|
|
84
|
+
config.openapi_version = "3.0.1"
|
|
85
|
+
config.servers = [{ "url" => "http://localhost:3000" }]
|
|
86
|
+
|
|
87
|
+
# Auth for rswag / OpenAPI (optional — remove if you have no auth)
|
|
88
|
+
config.security_schemes = {
|
|
89
|
+
"bearerAuth" => {
|
|
90
|
+
"type" => "http",
|
|
91
|
+
"scheme" => "bearer",
|
|
92
|
+
"bearerFormat" => "JWT"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
config.security = [{ "bearerAuth" => [] }]
|
|
96
|
+
|
|
97
|
+
# Behavior flags
|
|
98
|
+
config.with_config = true
|
|
99
|
+
config.with_multiple_examples = true
|
|
100
|
+
config.with_rspec_examples = true
|
|
101
|
+
config.with_response_description = true
|
|
102
|
+
config.with_payload_properties = true
|
|
103
|
+
config.action_for_old_examples = :append
|
|
104
|
+
|
|
105
|
+
# Env var names / environment gate
|
|
106
|
+
config.swagger_path_environment_variable = "SWAGGER_GENERATE_PATH"
|
|
107
|
+
config.generate_swagger_environment_variable = "SWAGGER_GENERATE"
|
|
108
|
+
config.environment_name = :test
|
|
109
|
+
config.auto_include = true
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 4. Combine fragments → single `swagger.yaml`
|
|
114
|
+
|
|
115
|
+
`SWAGGER_GENERATE=1` writes **one file per resource** (e.g. `swagger/v1/users.yaml`). Rswag UI expects a **single** file at `swagger/v1/swagger.yaml`. Add a small combiner in the host app.
|
|
116
|
+
|
|
117
|
+
#### `lib/swagger_combiner.rb`
|
|
57
118
|
|
|
58
119
|
```ruby
|
|
59
|
-
#
|
|
60
|
-
|
|
120
|
+
# frozen_string_literal: true
|
|
121
|
+
|
|
122
|
+
require "yaml"
|
|
123
|
+
require "fileutils"
|
|
124
|
+
|
|
125
|
+
# Merges every OpenAPI fragment under swagger/v1/*.yaml (except swagger.yaml)
|
|
126
|
+
# into a single swagger/v1/swagger.yaml for Rswag UI.
|
|
127
|
+
module SwaggerCombiner
|
|
128
|
+
module_function
|
|
129
|
+
|
|
130
|
+
SOURCE_DIR = File.expand_path("../swagger/v1", __dir__)
|
|
131
|
+
OUTPUT_FILE = File.join(SOURCE_DIR, "swagger.yaml")
|
|
132
|
+
SKIP_BASENAMES = %w[swagger.yaml swagger.yml].freeze
|
|
133
|
+
|
|
134
|
+
DEFAULT_DOC = {
|
|
135
|
+
"openapi" => "3.0.1",
|
|
136
|
+
"info" => {
|
|
137
|
+
"title" => "My API",
|
|
138
|
+
"description" => "Public HTTP API",
|
|
139
|
+
"version" => "1.0.0"
|
|
140
|
+
},
|
|
141
|
+
"servers" => [
|
|
142
|
+
{ "url" => "http://localhost:3000" }
|
|
143
|
+
],
|
|
144
|
+
"components" => {
|
|
145
|
+
"securitySchemes" => {
|
|
146
|
+
"bearerAuth" => {
|
|
147
|
+
"type" => "http",
|
|
148
|
+
"scheme" => "bearer",
|
|
149
|
+
"bearerFormat" => "JWT"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"security" => [
|
|
154
|
+
{ "bearerAuth" => [] }
|
|
155
|
+
],
|
|
156
|
+
"paths" => {}
|
|
157
|
+
}.freeze
|
|
158
|
+
|
|
159
|
+
def combine!(source_dir: SOURCE_DIR, output_file: OUTPUT_FILE)
|
|
160
|
+
FileUtils.mkdir_p(source_dir)
|
|
161
|
+
|
|
162
|
+
fragments = Dir[File.join(source_dir, "*.{yaml,yml}")]
|
|
163
|
+
.reject { |path| SKIP_BASENAMES.include?(File.basename(path)) }
|
|
164
|
+
.sort
|
|
165
|
+
|
|
166
|
+
if fragments.empty?
|
|
167
|
+
warn "[SwaggerCombiner] no fragment files found in #{source_dir}"
|
|
168
|
+
write_yaml(output_file, DEFAULT_DOC.dup)
|
|
169
|
+
return output_file
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
merged = deep_dup(DEFAULT_DOC)
|
|
173
|
+
|
|
174
|
+
fragments.each do |path|
|
|
175
|
+
doc = load_yaml(path)
|
|
176
|
+
next unless doc.is_a?(Hash)
|
|
177
|
+
|
|
178
|
+
merged["paths"] = deep_merge(merged["paths"], stringify_keys(doc["paths"] || {}))
|
|
179
|
+
merged["components"] = deep_merge(merged["components"], stringify_keys(doc["components"] || {}))
|
|
180
|
+
|
|
181
|
+
if doc["tags"].is_a?(Array)
|
|
182
|
+
merged["tags"] = Array(merged["tags"]) | doc["tags"]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
merged["paths"] = merged["paths"].sort.to_h
|
|
187
|
+
|
|
188
|
+
write_yaml(output_file, merged)
|
|
189
|
+
puts "[SwaggerCombiner] wrote #{output_file} (#{merged["paths"].size} paths from #{fragments.size} files)"
|
|
190
|
+
output_file
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def load_yaml(path)
|
|
194
|
+
YAML.safe_load(
|
|
195
|
+
File.read(path),
|
|
196
|
+
permitted_classes: [Date, Time, Symbol],
|
|
197
|
+
aliases: true
|
|
198
|
+
)
|
|
199
|
+
rescue Psych::DisallowedClass, Psych::BadAlias
|
|
200
|
+
YAML.load_file(path) # fragments may contain complex examples
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def write_yaml(path, data)
|
|
204
|
+
File.write(path, data.to_yaml)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def deep_merge(left, right)
|
|
208
|
+
left = {} unless left.is_a?(Hash)
|
|
209
|
+
right = {} unless right.is_a?(Hash)
|
|
210
|
+
|
|
211
|
+
left.merge(right) do |_key, old_val, new_val|
|
|
212
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
213
|
+
deep_merge(old_val, new_val)
|
|
214
|
+
else
|
|
215
|
+
new_val.nil? ? old_val : new_val
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def stringify_keys(value)
|
|
221
|
+
case value
|
|
222
|
+
when Hash
|
|
223
|
+
value.each_with_object({}) do |(k, v), memo|
|
|
224
|
+
memo[k.to_s] = stringify_keys(v)
|
|
225
|
+
end
|
|
226
|
+
when Array
|
|
227
|
+
value.map { |item| stringify_keys(item) }
|
|
228
|
+
else
|
|
229
|
+
value
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def deep_dup(value)
|
|
234
|
+
Marshal.load(Marshal.dump(value))
|
|
235
|
+
end
|
|
236
|
+
end
|
|
61
237
|
```
|
|
62
238
|
|
|
63
|
-
|
|
239
|
+
#### `config/initializers/combine_swagger.rb`
|
|
240
|
+
|
|
241
|
+
Rebuilds the combined file on every boot so `/api-docs` stays fresh.
|
|
64
242
|
|
|
65
243
|
```ruby
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
244
|
+
# frozen_string_literal: true
|
|
245
|
+
|
|
246
|
+
# Rebuild swagger/v1/swagger.yaml from per-resource fragments on every boot.
|
|
247
|
+
# Rswag UI expects a single file at /api-docs/v1/swagger.yaml.
|
|
248
|
+
begin
|
|
249
|
+
require Rails.root.join("lib/swagger_combiner")
|
|
250
|
+
SwaggerCombiner.combine!(
|
|
251
|
+
source_dir: Rails.root.join("swagger/v1").to_s,
|
|
252
|
+
output_file: Rails.root.join("swagger/v1/swagger.yaml").to_s
|
|
253
|
+
)
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
warn "[SwaggerCombiner] failed to combine OpenAPI docs: #{e.class}: #{e.message}"
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `lib/tasks/swagger.rake`
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# frozen_string_literal: true
|
|
263
|
+
|
|
264
|
+
namespace :swagger do
|
|
265
|
+
desc "Combine swagger/v1/*.yaml fragments into swagger/v1/swagger.yaml"
|
|
266
|
+
task combine: :environment do
|
|
267
|
+
require Rails.root.join("lib/swagger_combiner")
|
|
268
|
+
SwaggerCombiner.combine!
|
|
269
|
+
end
|
|
69
270
|
end
|
|
70
271
|
```
|
|
71
272
|
|
|
72
|
-
|
|
273
|
+
#### `bin/combine_swagger`
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
#!/usr/bin/env ruby
|
|
277
|
+
# frozen_string_literal: true
|
|
73
278
|
|
|
74
|
-
|
|
279
|
+
require_relative "../lib/swagger_combiner"
|
|
280
|
+
|
|
281
|
+
SwaggerCombiner.combine!
|
|
282
|
+
```
|
|
75
283
|
|
|
76
284
|
```bash
|
|
77
|
-
|
|
285
|
+
chmod +x bin/combine_swagger
|
|
78
286
|
```
|
|
79
287
|
|
|
80
|
-
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Generate docs
|
|
291
|
+
|
|
292
|
+
In **test**, the gem Railtie includes itself into `ApplicationController` automatically.
|
|
81
293
|
|
|
82
294
|
```bash
|
|
295
|
+
# One YAML fragment per controller tag under swagger/v1/
|
|
83
296
|
SWAGGER_GENERATE=1 bundle exec rspec spec/requests/
|
|
84
|
-
|
|
297
|
+
|
|
298
|
+
# Then combine into swagger/v1/swagger.yaml
|
|
299
|
+
bin/combine_swagger
|
|
300
|
+
# or: bundle exec rake swagger:combine
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Or write a single file directly:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
SWAGGER_GENERATE_PATH='swagger/v1/swagger.yaml' bundle exec rspec spec/requests/users_spec.rb
|
|
85
307
|
```
|
|
86
308
|
|
|
87
309
|
Generation only runs when:
|
|
@@ -91,80 +313,46 @@ Generation only runs when:
|
|
|
91
313
|
|
|
92
314
|
Without those env vars, specs run normally and nothing is written.
|
|
93
315
|
|
|
94
|
-
|
|
316
|
+
Open the UI: `http://localhost:3000/api-docs`
|
|
95
317
|
|
|
96
|
-
|
|
318
|
+
### Environment variables
|
|
319
|
+
|
|
320
|
+
| Variable | Purpose |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `SWAGGER_GENERATE_PATH` | Exact `.yaml`/`.yml` file **or** a directory (one file per tag) |
|
|
323
|
+
| `SWAGGER_GENERATE` | Write under `config.default_path` (rswag-aware) |
|
|
324
|
+
| `tag` | Optional override for the OpenAPI tag / filename stem |
|
|
325
|
+
|
|
326
|
+
### Manual include / disable auto-include
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# app/controllers/application_controller.rb
|
|
330
|
+
include SwaggerAutogenerate if Rails.env.test?
|
|
331
|
+
```
|
|
97
332
|
|
|
98
333
|
```ruby
|
|
99
334
|
# config/initializers/swagger_autogenerate.rb
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
config.default_path = 'swagger/v1'
|
|
335
|
+
config.auto_include = false
|
|
336
|
+
```
|
|
103
337
|
|
|
104
|
-
|
|
105
|
-
config.info_title = 'My API'
|
|
106
|
-
config.info_description = 'Public HTTP API'
|
|
107
|
-
config.info_version = '1.0.0'
|
|
108
|
-
config.openapi_version = '3.0.1'
|
|
109
|
-
config.servers = [{ 'url' => 'https://api.example.com' }]
|
|
338
|
+
---
|
|
110
339
|
|
|
111
|
-
|
|
112
|
-
config.security_schemes = {
|
|
113
|
-
'bearerAuth' => {
|
|
114
|
-
'type' => 'http',
|
|
115
|
-
'scheme' => 'bearer',
|
|
116
|
-
'bearerFormat' => 'JWT'
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
config.security = [{ 'bearerAuth' => [] }]
|
|
340
|
+
## Configuration reference
|
|
120
341
|
|
|
121
|
-
|
|
122
|
-
config.with_config = true # write openapi/info/components header
|
|
123
|
-
config.with_multiple_examples = true # keep several response examples per status
|
|
124
|
-
config.with_rspec_examples = true # use RSpec example group description as example name
|
|
125
|
-
config.with_response_description = true # human-readable status descriptions
|
|
126
|
-
config.with_payload_properties = true # document request payload shapes per example
|
|
127
|
-
config.action_for_old_examples = :append # or :replace
|
|
128
|
-
|
|
129
|
-
# Env var names / environment gate (rarely changed)
|
|
130
|
-
config.swagger_path_environment_variable = 'SWAGGER_GENERATE_PATH'
|
|
131
|
-
config.generate_swagger_environment_variable = 'SWAGGER_GENERATE'
|
|
132
|
-
config.environment_name = :test
|
|
133
|
-
config.auto_include = true
|
|
134
|
-
end
|
|
135
|
-
```
|
|
342
|
+
Only override what you need. Defaults are project-agnostic (empty security, app name as title, `swagger` output path).
|
|
136
343
|
|
|
137
344
|
### Full document override
|
|
138
345
|
|
|
139
|
-
If you already maintain a root OpenAPI hash (or want zero magic):
|
|
140
|
-
|
|
141
346
|
```ruby
|
|
142
347
|
SwaggerAutogenerate.configure do |config|
|
|
143
348
|
config.swagger_config = {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
349
|
+
"openapi" => "3.0.1",
|
|
350
|
+
"info" => { "title" => "Custom", "version" => "1.0.0" },
|
|
351
|
+
"components" => { "securitySchemes" => {} }
|
|
147
352
|
}
|
|
148
353
|
end
|
|
149
354
|
```
|
|
150
355
|
|
|
151
|
-
### Environment variables
|
|
152
|
-
|
|
153
|
-
| Variable | Purpose |
|
|
154
|
-
|---|---|
|
|
155
|
-
| `SWAGGER_GENERATE_PATH` | Exact `.yaml`/`.yml` file **or** a directory (one file per tag) |
|
|
156
|
-
| `SWAGGER_GENERATE` | Write under `config.default_path` (rswag-aware) |
|
|
157
|
-
| `tag` | Optional override for the OpenAPI tag / filename stem |
|
|
158
|
-
|
|
159
|
-
## Example
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
SWAGGER_GENERATE_PATH='swagger/v1/swagger.yaml' \
|
|
163
|
-
bundle exec rspec spec/requests/employees_spec.rb
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Coverage in the YAML matches what your examples actually hit (paths, statuses, bodies, query/path params). Prefer request specs that exercise real routes.
|
|
167
|
-
|
|
168
356
|
## Library layout
|
|
169
357
|
|
|
170
358
|
```
|