openapi_minitest 0.1.0
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 +7 -0
- data/.release-please-manifest.json +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +3 -0
- data/Makefile +17 -0
- data/README.md +356 -0
- data/Rakefile +10 -0
- data/lib/openapi_minitest/configuration.rb +55 -0
- data/lib/openapi_minitest/dsl.rb +204 -0
- data/lib/openapi_minitest/openapi/generator.rb +207 -0
- data/lib/openapi_minitest/railtie.rb +36 -0
- data/lib/openapi_minitest/result_collector.rb +186 -0
- data/lib/openapi_minitest/version.rb +5 -0
- data/lib/openapi_minitest.rb +14 -0
- data/release-please-config.json +17 -0
- data/sig/openapi_minitest.rbs +4 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7e0b37db854964620741255ac8d97c19570c3425977a9e6cb507de3a4212bfca
|
|
4
|
+
data.tar.gz: 7e03765531c01ebefb133856ab647e1626527f828481ed4d682cd8aa7896e49f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f666ddf8f5cb8e3986346d84b5920d7c07e7eb12a13c2f87324ebb7878d848a0bdd2f61dff0ca4611c713fad80cbd9e312e1a32019cc385058eaceaa2aa43d79
|
|
7
|
+
data.tar.gz: ac39441a39f4f8a8aa2828c7106f2c2ec34b101af98aca92d688a831ddbfd3d6404f75eff1234d3fb7832d66a8e8cda069c823c7d07d940621f1c020887ffd36
|
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-02-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add strict validation to catch undocumented fields ([767855b](https://github.com/k0va1/openapi_minitest/commit/767855b88d4d2c92fc633f366e6d71fddfd6232b))
|
|
9
|
+
* initial implementation of openapi_minitest gem ([b41acd7](https://github.com/k0va1/openapi_minitest/commit/b41acd7c2ab2cfdb17c68f2ecc99771856c34c33))
|
|
10
|
+
* output YAML format by default ([df930cf](https://github.com/k0va1/openapi_minitest/commit/df930cf925b73800dc628e3361019dc8bc453fc5))
|
|
11
|
+
* update to OpenAPI 3.1.0 specification ([778e5f3](https://github.com/k0va1/openapi_minitest/commit/778e5f3166066c50878b7b0cc1e6b00746643cdd))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* force initial release version to 0.1.0 ([a5b821d](https://github.com/k0va1/openapi_minitest/commit/a5b821decdea4a014cb12a2430ca33bb9ef4bc08))
|
|
17
|
+
* reset ResultCollector between tests to prevent test pollution ([ecc5bc2](https://github.com/k0va1/openapi_minitest/commit/ecc5bc268a6e8833eebdc6566441c0032b93f2ec))
|
|
18
|
+
* set manifest version to 0.0.0 for initial release as 0.1.0 ([7e340df](https://github.com/k0va1/openapi_minitest/commit/7e340dfdecc3bafdd1b3bf6147fc9e92e2e10f7e))
|
|
19
|
+
* use security schemes instead of Authorization header parameter ([eb092ef](https://github.com/k0va1/openapi_minitest/commit/eb092efaa34ae92745c890d88dea18b8b55ca7f8))
|
|
20
|
+
|
|
21
|
+
## Changelog
|
data/CLAUDE.md
ADDED
data/Makefile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# OpenapiMinitest
|
|
2
|
+
|
|
3
|
+
Generate OpenAPI 3.1 documentation from your Minitest integration tests. No DSL, no magic - just one helper method.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "openapi_minitest"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Configure (optional)
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# config/initializers/openapi_minitest.rb (Rails)
|
|
25
|
+
# or test/support/openapi.rb
|
|
26
|
+
|
|
27
|
+
OpenapiMinitest.configure do |config|
|
|
28
|
+
config.title = "My API"
|
|
29
|
+
config.version = "1.0.0"
|
|
30
|
+
config.description = "API documentation"
|
|
31
|
+
config.output_path = "doc/openapi.yml" # YAML by default
|
|
32
|
+
config.servers = ["https://api.example.com"]
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Define Schemas
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
OpenapiMinitest.define_schema :User, {
|
|
40
|
+
type: :object,
|
|
41
|
+
properties: {
|
|
42
|
+
id: { type: :integer },
|
|
43
|
+
email: { type: :string, format: :email },
|
|
44
|
+
name: { type: :string }
|
|
45
|
+
},
|
|
46
|
+
required: %w[id email]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
OpenapiMinitest.define_schema :UserList, {
|
|
50
|
+
type: :object,
|
|
51
|
+
properties: {
|
|
52
|
+
data: { type: :array, items: { "$ref" => "#/components/schemas/User" } },
|
|
53
|
+
meta: {
|
|
54
|
+
type: :object,
|
|
55
|
+
properties: {
|
|
56
|
+
total: { type: :integer },
|
|
57
|
+
page: { type: :integer }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
OpenapiMinitest.define_schema :Error, {
|
|
64
|
+
type: :object,
|
|
65
|
+
properties: {
|
|
66
|
+
error: { type: :string },
|
|
67
|
+
code: { type: :integer }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Write Tests
|
|
73
|
+
|
|
74
|
+
Write normal Minitest tests. Call `document_response` after each request you want documented:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class UsersApiTest < ActionDispatch::IntegrationTest
|
|
78
|
+
def test_returns_users
|
|
79
|
+
create(:user, name: "John")
|
|
80
|
+
create(:user, name: "Jane")
|
|
81
|
+
|
|
82
|
+
get "/api/users", headers: auth_headers
|
|
83
|
+
|
|
84
|
+
assert_response 200
|
|
85
|
+
document_response schema: :UserList, description: "Returns all users"
|
|
86
|
+
|
|
87
|
+
body = JSON.parse(response.body)
|
|
88
|
+
assert_equal 2, body["data"].size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_filters_users_by_name
|
|
92
|
+
create(:user, name: "John")
|
|
93
|
+
create(:user, name: "Jane")
|
|
94
|
+
|
|
95
|
+
get "/api/users", params: { q: "John" }, headers: auth_headers
|
|
96
|
+
|
|
97
|
+
assert_response 200
|
|
98
|
+
document_response schema: :UserList, description: "Filters users by name"
|
|
99
|
+
|
|
100
|
+
body = JSON.parse(response.body)
|
|
101
|
+
assert_equal 1, body["data"].size
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_returns_single_user
|
|
105
|
+
user = create(:user, name: "John")
|
|
106
|
+
|
|
107
|
+
get "/api/users/#{user.id}", headers: auth_headers
|
|
108
|
+
|
|
109
|
+
assert_response 200
|
|
110
|
+
document_response schema: :User, description: "User found", tags: ["Users"]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_user_not_found
|
|
114
|
+
get "/api/users/999999", headers: auth_headers
|
|
115
|
+
|
|
116
|
+
assert_response 404
|
|
117
|
+
document_response schema: :Error, description: "User not found"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_creates_user
|
|
121
|
+
post "/api/users",
|
|
122
|
+
params: { user: { name: "New User", email: "new@example.com" } },
|
|
123
|
+
headers: auth_headers,
|
|
124
|
+
as: :json
|
|
125
|
+
|
|
126
|
+
assert_response 201
|
|
127
|
+
document_response schema: :User, description: "User created", tags: ["Users"]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_create_user_validation_error
|
|
131
|
+
post "/api/users",
|
|
132
|
+
params: { user: { name: "" } },
|
|
133
|
+
headers: auth_headers,
|
|
134
|
+
as: :json
|
|
135
|
+
|
|
136
|
+
assert_response 422
|
|
137
|
+
document_response schema: :Error, description: "Validation failed"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def auth_headers
|
|
143
|
+
{ "Authorization" => "Bearer #{generate_token}" }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4. Generate Documentation
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Run tests with documentation generation
|
|
152
|
+
OPENAPI_GENERATE=true rails test test/integration/
|
|
153
|
+
|
|
154
|
+
# Or use the rake task
|
|
155
|
+
rails openapi:generate
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### Configuration Options
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
OpenapiMinitest.configure do |config|
|
|
164
|
+
config.title = "My API" # API title
|
|
165
|
+
config.version = "1.0.0" # API version
|
|
166
|
+
config.description = "Description" # API description
|
|
167
|
+
config.output_path = "doc/openapi.yml" # Output file path (YAML by default)
|
|
168
|
+
config.servers = [ # Server URLs
|
|
169
|
+
"https://api.example.com",
|
|
170
|
+
{ url: "https://staging.example.com", description: "Staging" }
|
|
171
|
+
]
|
|
172
|
+
config.security_schemes = { # Security schemes
|
|
173
|
+
bearer: {
|
|
174
|
+
type: :http,
|
|
175
|
+
scheme: :bearer
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
config.validate_schema = true # Validate responses against schemas
|
|
179
|
+
config.strict_validation = false # Fail if response contains undocumented fields
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### document_response
|
|
184
|
+
|
|
185
|
+
Call after making a request to record it for documentation:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
document_response(
|
|
189
|
+
schema: :SchemaName, # Schema reference (Symbol) or inline schema (Hash)
|
|
190
|
+
summary: "Operation summary", # Defaults to test name
|
|
191
|
+
description: "Response desc", # Description of this response
|
|
192
|
+
tags: ["Tag1", "Tag2"], # Tags for grouping
|
|
193
|
+
operation_id: "getUsers", # Unique operation ID
|
|
194
|
+
deprecated: false, # Mark endpoint as deprecated
|
|
195
|
+
strict: nil # Override strict validation (true/false/nil for global config)
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Schema Definition
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Reference to another schema
|
|
203
|
+
OpenapiMinitest.define_schema :UserList, {
|
|
204
|
+
type: :object,
|
|
205
|
+
properties: {
|
|
206
|
+
data: { type: :array, items: { "$ref" => "#/components/schemas/User" } }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Inline schema in tests
|
|
211
|
+
document_response schema: {
|
|
212
|
+
type: :object,
|
|
213
|
+
properties: {
|
|
214
|
+
status: { type: :string }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Nullable fields (OpenAPI 3.1 syntax - use type array instead of nullable: true)
|
|
219
|
+
OpenapiMinitest.define_schema :Article, {
|
|
220
|
+
type: :object,
|
|
221
|
+
properties: {
|
|
222
|
+
id: { type: :integer },
|
|
223
|
+
title: { type: :string },
|
|
224
|
+
subtitle: { type: [:string, :null] }, # nullable string
|
|
225
|
+
published_at: { type: [:string, :null], format: :datetime }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Features
|
|
231
|
+
|
|
232
|
+
- **No DSL** - Just one helper method, write normal Minitest tests
|
|
233
|
+
- **Schema validation** - Optionally validate responses against schemas during tests
|
|
234
|
+
- **Strict validation** - Fail tests when responses contain undocumented fields
|
|
235
|
+
- **Auto-detection** - Automatically extracts path parameters, query params
|
|
236
|
+
- **Security handling** - Authorization headers automatically use configured security schemes
|
|
237
|
+
- **Multiple examples** - Each test becomes an example in the docs
|
|
238
|
+
- **Request bodies** - Captures POST/PUT/PATCH request bodies as examples
|
|
239
|
+
|
|
240
|
+
## Strict Validation
|
|
241
|
+
|
|
242
|
+
Enable strict validation to catch undocumented fields in API responses. This helps ensure your documentation stays in sync with your implementation.
|
|
243
|
+
|
|
244
|
+
When strict mode is enabled, the test will fail if the response contains any fields not defined in the schema:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Response does not match schema:
|
|
248
|
+
The property '#/' contains additional properties ["unexpected_field"] outside of the schema when none are allowed
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Global Configuration
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
OpenapiMinitest.configure do |config|
|
|
255
|
+
config.strict_validation = true # All schemas strictly validated
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Per-Call Override
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# Force strict validation for this endpoint
|
|
263
|
+
document_response schema: :User, strict: true
|
|
264
|
+
|
|
265
|
+
# Disable strict validation for this endpoint (e.g., third-party responses)
|
|
266
|
+
document_response schema: :ExternalData, strict: false
|
|
267
|
+
|
|
268
|
+
# Use global config setting (default behavior)
|
|
269
|
+
document_response schema: :User
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Strict validation works by automatically injecting `additionalProperties: false` into all object schemas during validation. This includes nested objects and objects within arrays. If a schema already defines `additionalProperties`, that value is preserved.
|
|
273
|
+
|
|
274
|
+
## How It Works
|
|
275
|
+
|
|
276
|
+
1. You write normal integration tests
|
|
277
|
+
2. Call `document_response` after requests you want documented
|
|
278
|
+
3. The gem captures:
|
|
279
|
+
- Request method, path, parameters, headers
|
|
280
|
+
- Response status, body
|
|
281
|
+
- Schema reference or definition
|
|
282
|
+
4. After tests run, generates OpenAPI 3.1 YAML
|
|
283
|
+
|
|
284
|
+
## Path Parameter Detection
|
|
285
|
+
|
|
286
|
+
The gem automatically detects numeric IDs in paths and converts them:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
/api/users/123 -> /api/users/{user_id}
|
|
290
|
+
/api/users/123/posts/456 -> /api/users/{user_id}/posts/{post_id}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Security / Authentication
|
|
294
|
+
|
|
295
|
+
When you configure `security_schemes` and your tests include an `Authorization` header, the gem automatically adds the `security` property to those operations:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
OpenapiMinitest.configure do |config|
|
|
299
|
+
config.security_schemes = {
|
|
300
|
+
bearer: {
|
|
301
|
+
type: :http,
|
|
302
|
+
scheme: :bearer
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Operations with Authorization headers will be generated with:
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
security:
|
|
312
|
+
- bearer: []
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Validator Compatibility
|
|
316
|
+
|
|
317
|
+
This gem generates OpenAPI 3.1.0 which supports type arrays for nullable fields:
|
|
318
|
+
|
|
319
|
+
```yaml
|
|
320
|
+
type:
|
|
321
|
+
- string
|
|
322
|
+
- 'null'
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Some validators may not fully support OpenAPI 3.1.0 yet. If you encounter validation errors about type arrays, ensure your validator supports OpenAPI 3.1.0.
|
|
326
|
+
|
|
327
|
+
## Non-Rails Usage
|
|
328
|
+
|
|
329
|
+
Include the DSL module manually:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class MyApiTest < Minitest::Test
|
|
333
|
+
include OpenapiMinitest::DSL
|
|
334
|
+
|
|
335
|
+
# ... your tests
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Generate documentation after tests
|
|
339
|
+
Minitest.after_run do
|
|
340
|
+
if ENV["OPENAPI_GENERATE"]
|
|
341
|
+
OpenapiMinitest::OpenAPI::Generator.new.write
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Development
|
|
347
|
+
|
|
348
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
349
|
+
|
|
350
|
+
## Contributing
|
|
351
|
+
|
|
352
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/k0va1/openapi_minitest.
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiMinitest
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :title,
|
|
6
|
+
:version,
|
|
7
|
+
:description,
|
|
8
|
+
:output_path,
|
|
9
|
+
:servers,
|
|
10
|
+
:security_schemes,
|
|
11
|
+
:tags,
|
|
12
|
+
:validate_schema,
|
|
13
|
+
:strict_validation
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@title = "API Documentation"
|
|
17
|
+
@version = "1.0.0"
|
|
18
|
+
@description = nil
|
|
19
|
+
@output_path = "doc/openapi.yml"
|
|
20
|
+
@servers = []
|
|
21
|
+
@security_schemes = {}
|
|
22
|
+
@tags = []
|
|
23
|
+
@validate_schema = true
|
|
24
|
+
@strict_validation = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def configuration
|
|
30
|
+
@configuration ||= Configuration.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield(configuration)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
@configuration = Configuration.new
|
|
39
|
+
@schemas = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Schema registry
|
|
43
|
+
def schemas
|
|
44
|
+
@schemas ||= {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def define_schema(name, definition)
|
|
48
|
+
schemas[name.to_sym] = definition
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def schema(name)
|
|
52
|
+
schemas[name.to_sym]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "json-schema"
|
|
5
|
+
|
|
6
|
+
module OpenapiMinitest
|
|
7
|
+
module DSL
|
|
8
|
+
# Records the current request/response for OpenAPI documentation.
|
|
9
|
+
#
|
|
10
|
+
# Call this after making a request and asserting the response status.
|
|
11
|
+
# It captures all the details needed for OpenAPI generation.
|
|
12
|
+
#
|
|
13
|
+
# @param schema [Symbol, Hash, nil] Schema name (references defined schema) or inline schema hash
|
|
14
|
+
# @param summary [String, nil] Short summary of the operation (defaults to test name)
|
|
15
|
+
# @param description [String, nil] Description of this specific response
|
|
16
|
+
# @param tags [Array<String>] Tags for grouping endpoints
|
|
17
|
+
# @param operation_id [String, nil] Unique operation identifier
|
|
18
|
+
# @param deprecated [Boolean] Whether this endpoint is deprecated
|
|
19
|
+
# @param strict [Boolean, nil] Strict validation mode - fails if response contains undocumented fields.
|
|
20
|
+
# When nil (default), uses the global config.strict_validation setting.
|
|
21
|
+
#
|
|
22
|
+
# @example Basic usage
|
|
23
|
+
# def test_returns_users
|
|
24
|
+
# get "/api/users"
|
|
25
|
+
# assert_response 200
|
|
26
|
+
# document_response schema: :UserList, description: "Returns all users"
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example With tags and summary
|
|
30
|
+
# def test_creates_user
|
|
31
|
+
# post "/api/users", params: { name: "John" }
|
|
32
|
+
# assert_response 201
|
|
33
|
+
# document_response schema: :User, summary: "Create user", tags: ["Users"]
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# @example Inline schema
|
|
37
|
+
# def test_health_check
|
|
38
|
+
# get "/health"
|
|
39
|
+
# assert_response 200
|
|
40
|
+
# document_response schema: { type: :object, properties: { status: { type: :string } } }
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @example Without schema (just documents the endpoint)
|
|
44
|
+
# def test_deletes_user
|
|
45
|
+
# delete "/api/users/1"
|
|
46
|
+
# assert_response 204
|
|
47
|
+
# document_response description: "User deleted"
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# @example Strict validation (fails if response has undocumented fields)
|
|
51
|
+
# def test_returns_user_strict
|
|
52
|
+
# get "/api/users/1"
|
|
53
|
+
# assert_response 200
|
|
54
|
+
# document_response schema: :User, strict: true
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
def document_response(schema: nil, summary: nil, description: nil, tags: [], operation_id: nil, deprecated: false, strict: nil)
|
|
58
|
+
# Validate schema if provided and validation is enabled
|
|
59
|
+
if schema && OpenapiMinitest.configuration.validate_schema && response.body.present?
|
|
60
|
+
use_strict = strict.nil? ? OpenapiMinitest.configuration.strict_validation : strict
|
|
61
|
+
validate_response_schema!(schema, strict: use_strict)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Record for OpenAPI generation
|
|
65
|
+
ResultCollector.instance.record(
|
|
66
|
+
request: request,
|
|
67
|
+
response: response,
|
|
68
|
+
schema: normalize_schema(schema),
|
|
69
|
+
summary: summary || generate_summary,
|
|
70
|
+
description: description,
|
|
71
|
+
tags: Array(tags),
|
|
72
|
+
operation_id: operation_id,
|
|
73
|
+
deprecated: deprecated,
|
|
74
|
+
test_name: name
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def validate_response_schema!(schema, strict: false)
|
|
81
|
+
resolved = resolve_schema(schema)
|
|
82
|
+
resolved = apply_strict_validation(resolved) if strict
|
|
83
|
+
body = JSON.parse(response.body)
|
|
84
|
+
|
|
85
|
+
errors = JSON::Validator.fully_validate(
|
|
86
|
+
stringify_keys(resolved),
|
|
87
|
+
body,
|
|
88
|
+
strict: false,
|
|
89
|
+
clear_cache: true
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return if errors.empty?
|
|
93
|
+
|
|
94
|
+
flunk "Response does not match schema:\n#{errors.join("\n")}\n\nBody: #{response.body}"
|
|
95
|
+
rescue JSON::ParserError => e
|
|
96
|
+
flunk "Response is not valid JSON: #{e.message}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def apply_strict_validation(schema)
|
|
100
|
+
case schema
|
|
101
|
+
when Hash
|
|
102
|
+
result = schema.transform_values { |v| apply_strict_validation(v) }
|
|
103
|
+
# Add additionalProperties: false to object types
|
|
104
|
+
if object_type?(result)
|
|
105
|
+
result[:additionalProperties] = false unless result.key?(:additionalProperties) || result.key?("additionalProperties")
|
|
106
|
+
end
|
|
107
|
+
result
|
|
108
|
+
when Array
|
|
109
|
+
schema.map { |item| apply_strict_validation(item) }
|
|
110
|
+
else
|
|
111
|
+
schema
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def object_type?(schema)
|
|
116
|
+
type = schema[:type] || schema["type"]
|
|
117
|
+
return false unless type
|
|
118
|
+
|
|
119
|
+
case type
|
|
120
|
+
when :object, "object"
|
|
121
|
+
true
|
|
122
|
+
when Array
|
|
123
|
+
type.any? { |t| t == :object || t == "object" }
|
|
124
|
+
else
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def resolve_schema(schema)
|
|
130
|
+
case schema
|
|
131
|
+
when Symbol
|
|
132
|
+
referenced = OpenapiMinitest.schema(schema)
|
|
133
|
+
raise ArgumentError, "Unknown schema: #{schema}" unless referenced
|
|
134
|
+
deep_resolve_refs(deep_dup(referenced))
|
|
135
|
+
when Hash
|
|
136
|
+
deep_resolve_refs(deep_dup(schema))
|
|
137
|
+
else
|
|
138
|
+
schema
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def deep_resolve_refs(obj)
|
|
143
|
+
case obj
|
|
144
|
+
when Hash
|
|
145
|
+
if obj[:$ref] || obj["$ref"]
|
|
146
|
+
ref = obj[:$ref] || obj["$ref"]
|
|
147
|
+
if ref.to_s.start_with?("#/components/schemas/")
|
|
148
|
+
schema_name = ref.to_s.split("/").last.to_sym
|
|
149
|
+
referenced = OpenapiMinitest.schema(schema_name)
|
|
150
|
+
if referenced
|
|
151
|
+
return deep_resolve_refs(deep_dup(referenced))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
obj.transform_values { |v| deep_resolve_refs(v) }
|
|
156
|
+
when Array
|
|
157
|
+
obj.map { |item| deep_resolve_refs(item) }
|
|
158
|
+
else
|
|
159
|
+
obj
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def deep_dup(obj)
|
|
164
|
+
case obj
|
|
165
|
+
when Hash
|
|
166
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
167
|
+
when Array
|
|
168
|
+
obj.map { |item| deep_dup(item) }
|
|
169
|
+
else
|
|
170
|
+
obj
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def stringify_keys(obj)
|
|
175
|
+
case obj
|
|
176
|
+
when Hash
|
|
177
|
+
obj.each_with_object({}) do |(key, value), result|
|
|
178
|
+
result[key.to_s] = stringify_keys(value)
|
|
179
|
+
end
|
|
180
|
+
when Array
|
|
181
|
+
obj.map { |item| stringify_keys(item) }
|
|
182
|
+
else
|
|
183
|
+
obj
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def normalize_schema(schema)
|
|
188
|
+
case schema
|
|
189
|
+
when Symbol
|
|
190
|
+
{"$ref" => "#/components/schemas/#{schema}"}
|
|
191
|
+
when Hash
|
|
192
|
+
stringify_keys(schema)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def generate_summary
|
|
197
|
+
# Convert test_creates_user -> "Creates user"
|
|
198
|
+
name.to_s
|
|
199
|
+
.sub(/^test_/, "")
|
|
200
|
+
.tr("_", " ")
|
|
201
|
+
.capitalize
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module OpenapiMinitest
|
|
7
|
+
module OpenAPI
|
|
8
|
+
class Generator
|
|
9
|
+
def initialize
|
|
10
|
+
@config = OpenapiMinitest.configuration
|
|
11
|
+
@collector = ResultCollector.instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
{
|
|
16
|
+
"openapi" => "3.1.0",
|
|
17
|
+
"info" => build_info,
|
|
18
|
+
"servers" => build_servers,
|
|
19
|
+
"paths" => build_paths,
|
|
20
|
+
"components" => build_components
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write
|
|
25
|
+
path = @config.output_path
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
27
|
+
File.write(path, generate.to_yaml)
|
|
28
|
+
path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def build_info
|
|
34
|
+
info = {
|
|
35
|
+
"title" => @config.title,
|
|
36
|
+
"version" => @config.version
|
|
37
|
+
}
|
|
38
|
+
info["description"] = @config.description if @config.description
|
|
39
|
+
info
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_servers
|
|
43
|
+
return nil if @config.servers.empty?
|
|
44
|
+
|
|
45
|
+
@config.servers.map do |server|
|
|
46
|
+
case server
|
|
47
|
+
when String
|
|
48
|
+
{"url" => server}
|
|
49
|
+
when Hash
|
|
50
|
+
stringify_keys(server)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_paths
|
|
56
|
+
paths = {}
|
|
57
|
+
|
|
58
|
+
@collector.operations.each do |key, operation|
|
|
59
|
+
path = operation[:path]
|
|
60
|
+
method = operation[:method]
|
|
61
|
+
|
|
62
|
+
paths[path] ||= {}
|
|
63
|
+
paths[path][method] = build_operation(key, operation)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
paths
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_operation(key, operation)
|
|
70
|
+
op = {}
|
|
71
|
+
|
|
72
|
+
op["summary"] = operation[:summary] if operation[:summary]
|
|
73
|
+
op["tags"] = operation[:tags] if operation[:tags]&.any?
|
|
74
|
+
op["operationId"] = operation[:operation_id] if operation[:operation_id]
|
|
75
|
+
op["deprecated"] = true if operation[:deprecated]
|
|
76
|
+
|
|
77
|
+
# Security (if operation requires auth and security schemes are configured)
|
|
78
|
+
if operation[:requires_auth] && @config.security_schemes.any?
|
|
79
|
+
op["security"] = @config.security_schemes.keys.map { |scheme| {scheme.to_s => []} }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Parameters
|
|
83
|
+
if operation[:parameters]&.any?
|
|
84
|
+
op["parameters"] = operation[:parameters].map { |p| stringify_keys(p) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Request body (for POST/PUT/PATCH)
|
|
88
|
+
request_body = build_request_body(key, operation[:method])
|
|
89
|
+
op["requestBody"] = request_body if request_body
|
|
90
|
+
|
|
91
|
+
# Responses
|
|
92
|
+
op["responses"] = build_responses(key)
|
|
93
|
+
|
|
94
|
+
op
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_request_body(key, method)
|
|
98
|
+
return nil unless %w[post put patch].include?(method)
|
|
99
|
+
|
|
100
|
+
responses = @collector.responses[key]
|
|
101
|
+
return nil unless responses
|
|
102
|
+
|
|
103
|
+
# Find a request example from any response
|
|
104
|
+
example = nil
|
|
105
|
+
responses.each_value do |response_list|
|
|
106
|
+
response_list.each do |resp|
|
|
107
|
+
if resp[:request_example]
|
|
108
|
+
example = resp[:request_example]
|
|
109
|
+
break
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
break if example
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return nil unless example
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
"required" => true,
|
|
119
|
+
"content" => {
|
|
120
|
+
"application/json" => {
|
|
121
|
+
"schema" => {"type" => "object"},
|
|
122
|
+
"example" => example
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_responses(key)
|
|
129
|
+
responses_data = @collector.responses[key] || {}
|
|
130
|
+
responses = {}
|
|
131
|
+
|
|
132
|
+
responses_data.each do |status, response_list|
|
|
133
|
+
primary = response_list.first
|
|
134
|
+
|
|
135
|
+
response = {
|
|
136
|
+
"description" => primary[:description]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Add content with schema and examples
|
|
140
|
+
if primary[:schema] || primary[:example]
|
|
141
|
+
content = {}
|
|
142
|
+
|
|
143
|
+
content["schema"] = primary[:schema] if primary[:schema]
|
|
144
|
+
|
|
145
|
+
# Build examples from all responses with this status
|
|
146
|
+
if response_list.any? { |r| r[:example] }
|
|
147
|
+
examples = {}
|
|
148
|
+
response_list.each do |resp|
|
|
149
|
+
next unless resp[:example]
|
|
150
|
+
example_name = resp[:test_name] || "example"
|
|
151
|
+
examples[example_name] = {
|
|
152
|
+
"value" => resp[:example]
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
content["examples"] = examples if examples.any?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
response["content"] = {
|
|
159
|
+
"application/json" => content
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
responses[status] = response
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Ensure at least one response
|
|
167
|
+
responses["200"] = {"description" => "Successful response"} if responses.empty?
|
|
168
|
+
|
|
169
|
+
responses
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_components
|
|
173
|
+
components = {}
|
|
174
|
+
|
|
175
|
+
# Add registered schemas
|
|
176
|
+
if OpenapiMinitest.schemas.any?
|
|
177
|
+
components["schemas"] = {}
|
|
178
|
+
OpenapiMinitest.schemas.each do |name, schema|
|
|
179
|
+
components["schemas"][name.to_s] = stringify_keys(schema)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Add security schemes
|
|
184
|
+
if @config.security_schemes.any?
|
|
185
|
+
components["securitySchemes"] = stringify_keys(@config.security_schemes)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
components.empty? ? nil : components
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def stringify_keys(obj)
|
|
192
|
+
case obj
|
|
193
|
+
when Hash
|
|
194
|
+
obj.each_with_object({}) do |(key, value), result|
|
|
195
|
+
result[key.to_s] = stringify_keys(value)
|
|
196
|
+
end
|
|
197
|
+
when Array
|
|
198
|
+
obj.map { |item| stringify_keys(item) }
|
|
199
|
+
when Symbol
|
|
200
|
+
obj.to_s
|
|
201
|
+
else
|
|
202
|
+
obj
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiMinitest
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
railtie_name :openapi_minitest
|
|
6
|
+
|
|
7
|
+
rake_tasks do
|
|
8
|
+
namespace :openapi do
|
|
9
|
+
desc "Generate OpenAPI documentation from tests"
|
|
10
|
+
task generate: :environment do
|
|
11
|
+
ENV["OPENAPI_GENERATE"] = "true"
|
|
12
|
+
|
|
13
|
+
require "rails/test_unit/runner"
|
|
14
|
+
Rails::TestUnit::Runner.run(ARGV)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "openapi_minitest.configure" do
|
|
20
|
+
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
21
|
+
include OpenapiMinitest::DSL
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
config.after_initialize do
|
|
26
|
+
if Rails.env.test?
|
|
27
|
+
Minitest.after_run do
|
|
28
|
+
if ENV["OPENAPI_GENERATE"] == "true" && !OpenapiMinitest::ResultCollector.instance.empty?
|
|
29
|
+
path = OpenapiMinitest::OpenAPI::Generator.new.write
|
|
30
|
+
puts "\nOpenAPI documentation generated: #{path}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module OpenapiMinitest
|
|
7
|
+
class ResultCollector
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
reset!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset!
|
|
15
|
+
@operations = {} # "METHOD /path" => operation data
|
|
16
|
+
@responses = {} # "METHOD /path" => { status => [response_data] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record(request:, response:, schema:, summary:, description:, tags:, operation_id:, deprecated:, test_name:)
|
|
20
|
+
method = request.request_method.downcase
|
|
21
|
+
path = normalize_path(request.path)
|
|
22
|
+
key = "#{method} #{path}"
|
|
23
|
+
|
|
24
|
+
# Store operation metadata (first one wins for summary, tags merge)
|
|
25
|
+
@operations[key] ||= {
|
|
26
|
+
method: method,
|
|
27
|
+
path: path,
|
|
28
|
+
summary: summary,
|
|
29
|
+
tags: [],
|
|
30
|
+
operation_id: operation_id,
|
|
31
|
+
deprecated: deprecated,
|
|
32
|
+
parameters: extract_parameters(request, path),
|
|
33
|
+
requires_auth: has_authorization_header?(request)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Merge tags from all tests
|
|
37
|
+
@operations[key][:tags] = (@operations[key][:tags] + tags).uniq
|
|
38
|
+
|
|
39
|
+
# Store response data grouped by status
|
|
40
|
+
status = response.status.to_s
|
|
41
|
+
@responses[key] ||= {}
|
|
42
|
+
@responses[key][status] ||= []
|
|
43
|
+
|
|
44
|
+
@responses[key][status] << {
|
|
45
|
+
description: description || default_description(response.status),
|
|
46
|
+
schema: schema,
|
|
47
|
+
example: parse_body(response.body),
|
|
48
|
+
test_name: test_name,
|
|
49
|
+
request_example: extract_request_example(request)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :operations
|
|
54
|
+
|
|
55
|
+
attr_reader :responses
|
|
56
|
+
|
|
57
|
+
def empty?
|
|
58
|
+
@operations.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def normalize_path(path)
|
|
64
|
+
# Convert /api/users/123 to /api/users/{id}
|
|
65
|
+
# Convert /api/users/123/posts/456 to /api/users/{id}/posts/{post_id}
|
|
66
|
+
segments = path.split("/")
|
|
67
|
+
normalized = []
|
|
68
|
+
param_counts = Hash.new(0)
|
|
69
|
+
|
|
70
|
+
segments.each_with_index do |segment, index|
|
|
71
|
+
if segment.match?(/^\d+$/)
|
|
72
|
+
# This is an ID - figure out what to call it
|
|
73
|
+
prev_segment = segments[index - 1]
|
|
74
|
+
if prev_segment
|
|
75
|
+
# Singularize: users -> user_id, posts -> post_id
|
|
76
|
+
param_name = singularize(prev_segment) + "_id"
|
|
77
|
+
# Handle multiple params of same type
|
|
78
|
+
if param_counts[param_name] > 0
|
|
79
|
+
param_name = "#{param_name}_#{param_counts[param_name] + 1}"
|
|
80
|
+
end
|
|
81
|
+
param_counts[param_name] += 1
|
|
82
|
+
# First occurrence of a resource ID is just {id} for cleaner paths
|
|
83
|
+
param_name = "id" if param_counts[param_name] == 1 && normalized.size == segments.size - 2
|
|
84
|
+
normalized << "{#{param_name}}"
|
|
85
|
+
else
|
|
86
|
+
normalized << "{id}"
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
normalized << segment
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
normalized.join("/")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def singularize(word)
|
|
97
|
+
# Basic singularization - in Rails app, would use ActiveSupport
|
|
98
|
+
if word.end_with?("ies")
|
|
99
|
+
word[0..-4] + "y"
|
|
100
|
+
elsif word.end_with?("ses", "xes", "zes", "ches", "shes")
|
|
101
|
+
word[0..-3]
|
|
102
|
+
elsif word.end_with?("s") && !word.end_with?("ss")
|
|
103
|
+
word[0..-2]
|
|
104
|
+
else
|
|
105
|
+
word
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_parameters(request, normalized_path)
|
|
110
|
+
params = []
|
|
111
|
+
|
|
112
|
+
# Path parameters
|
|
113
|
+
normalized_path.scan(/\{(\w+)\}/).flatten.each do |param_name|
|
|
114
|
+
params << {
|
|
115
|
+
name: param_name,
|
|
116
|
+
in: "path",
|
|
117
|
+
required: true,
|
|
118
|
+
schema: {type: "string"}
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Query parameters
|
|
123
|
+
if request.query_parameters.any?
|
|
124
|
+
request.query_parameters.each do |name, value|
|
|
125
|
+
params << {
|
|
126
|
+
name: name.to_s,
|
|
127
|
+
in: "query",
|
|
128
|
+
required: false,
|
|
129
|
+
schema: infer_schema_type(value)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Note: Authorization header is handled via security schemes, not parameters
|
|
135
|
+
|
|
136
|
+
params
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def has_authorization_header?(request)
|
|
140
|
+
!!(request.headers["Authorization"] || request.headers["HTTP_AUTHORIZATION"])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def infer_schema_type(value)
|
|
144
|
+
case value
|
|
145
|
+
when Integer then {type: "integer"}
|
|
146
|
+
when Float then {type: "number"}
|
|
147
|
+
when TrueClass, FalseClass then {type: "boolean"}
|
|
148
|
+
when Array then {type: "array", items: {type: "string"}}
|
|
149
|
+
else {type: "string"}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def extract_request_example(request)
|
|
154
|
+
return nil if request.request_method == "GET"
|
|
155
|
+
|
|
156
|
+
body = request.body.read
|
|
157
|
+
request.body.rewind
|
|
158
|
+
return nil if body.empty?
|
|
159
|
+
|
|
160
|
+
JSON.parse(body)
|
|
161
|
+
rescue JSON::ParserError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_body(body)
|
|
166
|
+
return nil if body.nil? || body.empty?
|
|
167
|
+
JSON.parse(body)
|
|
168
|
+
rescue JSON::ParserError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def default_description(status)
|
|
173
|
+
{
|
|
174
|
+
200 => "Successful response",
|
|
175
|
+
201 => "Created",
|
|
176
|
+
204 => "No content",
|
|
177
|
+
400 => "Bad request",
|
|
178
|
+
401 => "Unauthorized",
|
|
179
|
+
403 => "Forbidden",
|
|
180
|
+
404 => "Not found",
|
|
181
|
+
422 => "Unprocessable entity",
|
|
182
|
+
500 => "Internal server error"
|
|
183
|
+
}[status] || "Response"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openapi_minitest/version"
|
|
4
|
+
require_relative "openapi_minitest/configuration"
|
|
5
|
+
require_relative "openapi_minitest/result_collector"
|
|
6
|
+
require_relative "openapi_minitest/dsl"
|
|
7
|
+
require_relative "openapi_minitest/openapi/generator"
|
|
8
|
+
|
|
9
|
+
require_relative "openapi_minitest/railtie" if defined?(Rails::Railtie)
|
|
10
|
+
|
|
11
|
+
module OpenapiMinitest
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class SchemaValidationError < Error; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packages": {
|
|
3
|
+
".": {
|
|
4
|
+
"package-name": "openapi_minitest",
|
|
5
|
+
"include-component-in-tag": false,
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"release-type": "ruby",
|
|
8
|
+
"bump-minor-pre-major": true,
|
|
9
|
+
"bump-patch-for-minor-pre-major": true,
|
|
10
|
+
"draft": false,
|
|
11
|
+
"prerelease": false,
|
|
12
|
+
"version-file": "lib/openapi_minitest/version.rb",
|
|
13
|
+
"release-as": "0.1.0"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
17
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: openapi_minitest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alex Koval
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json-schema
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bigdecimal
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: A Ruby gem that generates OpenAPI 3.0 documentation from Minitest integration
|
|
55
|
+
tests in Rails applications. Uses serializers as the single source of truth for
|
|
56
|
+
response schemas.
|
|
57
|
+
email:
|
|
58
|
+
- al3xander.koval@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".release-please-manifest.json"
|
|
64
|
+
- ".standard.yml"
|
|
65
|
+
- CHANGELOG.md
|
|
66
|
+
- CLAUDE.md
|
|
67
|
+
- Makefile
|
|
68
|
+
- README.md
|
|
69
|
+
- Rakefile
|
|
70
|
+
- lib/openapi_minitest.rb
|
|
71
|
+
- lib/openapi_minitest/configuration.rb
|
|
72
|
+
- lib/openapi_minitest/dsl.rb
|
|
73
|
+
- lib/openapi_minitest/openapi/generator.rb
|
|
74
|
+
- lib/openapi_minitest/railtie.rb
|
|
75
|
+
- lib/openapi_minitest/result_collector.rb
|
|
76
|
+
- lib/openapi_minitest/version.rb
|
|
77
|
+
- release-please-config.json
|
|
78
|
+
- sig/openapi_minitest.rbs
|
|
79
|
+
homepage: https://github.com/k0va1/openapi_minitest
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata:
|
|
83
|
+
homepage_uri: https://github.com/k0va1/openapi_minitest
|
|
84
|
+
source_code_uri: https://github.com/k0va1/openapi_minitest
|
|
85
|
+
changelog_uri: https://github.com/k0va1/openapi_minitest/blob/master/CHANGELOG.md
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 3.2.0
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.6.9
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Generate OpenAPI documentation from Minitest integration tests
|
|
103
|
+
test_files: []
|