miniswag 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/Gemfile +5 -0
- data/MIT-LICENSE +21 -0
- data/README.md +255 -0
- data/lib/generators/miniswag/install/install_generator.rb +13 -0
- data/lib/generators/miniswag/install/templates/openapi_helper.rb +27 -0
- data/lib/miniswag/configuration.rb +37 -0
- data/lib/miniswag/dsl.rb +271 -0
- data/lib/miniswag/extended_schema.rb +21 -0
- data/lib/miniswag/minitest_plugin.rb +33 -0
- data/lib/miniswag/openapi_generator.rb +250 -0
- data/lib/miniswag/railtie.rb +13 -0
- data/lib/miniswag/request_factory.rb +217 -0
- data/lib/miniswag/response_validator.rb +75 -0
- data/lib/miniswag/test_case.rb +19 -0
- data/lib/miniswag/version.rb +5 -0
- data/lib/miniswag.rb +33 -0
- data/lib/tasks/miniswag_tasks.rake +27 -0
- metadata +157 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0b4952ed36467e585a435344a1742e8c78f0ff8a4fd1c0696690b78327872952
|
|
4
|
+
data.tar.gz: e01410bb9555e9de1c30c73a6731b817ff427593e8b2103611c2b74945b69b61
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0a1949b5278bd353be7bfa254218c02f0e780b99dd46d7769c3f033748a67fc6eadb884a0184c17fadf373b908fb89deba81ceeba960f86492c0f38672596065
|
|
7
|
+
data.tar.gz: bea825c1a547919423950a6b81d71b85af84baccc76516d5e9c3956de81a6848a6afee4dd735bbc85c4461549118a997c9d7bfb5af2f14c096ca251d922a15c1
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sika
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Miniswag
|
|
2
|
+
|
|
3
|
+
OpenAPI (Swagger) documentation DSL for **Minitest**. A port of [rswag](https://github.com/rswag/rswag) that works with Minitest instead of RSpec.
|
|
4
|
+
|
|
5
|
+
Write API integration tests that simultaneously validate your API responses and generate OpenAPI 3.x specification files — no RSpec required.
|
|
6
|
+
|
|
7
|
+
## Gems
|
|
8
|
+
|
|
9
|
+
| Gem | Description |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `miniswag` | Core DSL and OpenAPI spec generator (replaces `rswag-specs`) |
|
|
12
|
+
| `miniswag-api` | Rails engine that serves OpenAPI files as JSON/YAML endpoints (replaces `rswag-api`) |
|
|
13
|
+
| `miniswag-ui` | Rails engine that serves Swagger UI powered by your OpenAPI specs (replaces `rswag-ui`) |
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# Core — test DSL + spec generation
|
|
21
|
+
gem "miniswag", group: :test
|
|
22
|
+
|
|
23
|
+
# Optional — serve specs and Swagger UI in your app
|
|
24
|
+
gem "miniswag-api"
|
|
25
|
+
gem "miniswag-ui"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
rails generate miniswag:install # creates test/openapi_helper.rb
|
|
33
|
+
rails generate miniswag:api:install # creates config/initializers/miniswag_api.rb
|
|
34
|
+
rails generate miniswag:ui:install # creates config/initializers/miniswag_ui.rb
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### 1. Configure your OpenAPI specs
|
|
40
|
+
|
|
41
|
+
Edit `test/openapi_helper.rb`:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "miniswag"
|
|
45
|
+
|
|
46
|
+
Miniswag.configure do |config|
|
|
47
|
+
config.openapi_root = Rails.root.join("docs/api").to_s
|
|
48
|
+
|
|
49
|
+
config.openapi_specs = {
|
|
50
|
+
"v1.yaml" => {
|
|
51
|
+
openapi: "3.0.1",
|
|
52
|
+
info: { title: "My API", version: "v1" },
|
|
53
|
+
servers: [{ url: "https://api.example.com" }]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Write a test
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "openapi_helper"
|
|
63
|
+
|
|
64
|
+
class PetsTest < Miniswag::TestCase
|
|
65
|
+
path "/pets" do
|
|
66
|
+
get "Lists all pets" do
|
|
67
|
+
tags "Pets"
|
|
68
|
+
produces "application/json"
|
|
69
|
+
|
|
70
|
+
response 200, "successful" do
|
|
71
|
+
schema type: :array, items: { "$ref" => "#/components/schemas/Pet" }
|
|
72
|
+
|
|
73
|
+
run_test!
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
post "Creates a pet" do
|
|
78
|
+
tags "Pets"
|
|
79
|
+
consumes "application/json"
|
|
80
|
+
parameter name: :body, in: :body, schema: {
|
|
81
|
+
type: :object,
|
|
82
|
+
properties: {
|
|
83
|
+
name: { type: :string },
|
|
84
|
+
age: { type: :integer }
|
|
85
|
+
},
|
|
86
|
+
required: %w[name]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
response 201, "pet created" do
|
|
90
|
+
params { { body: { name: "Fido", age: 3 } } }
|
|
91
|
+
|
|
92
|
+
run_test!
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
response 422, "invalid request" do
|
|
96
|
+
params { { body: { age: -1 } } }
|
|
97
|
+
|
|
98
|
+
run_test!
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. Generate OpenAPI specs
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
rake miniswag:swaggerize
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This runs your tests and writes the resulting OpenAPI files to `openapi_root`.
|
|
112
|
+
|
|
113
|
+
## DSL Reference
|
|
114
|
+
|
|
115
|
+
The DSL mirrors rswag closely. Key differences from rswag are noted below.
|
|
116
|
+
|
|
117
|
+
### Structure
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class MyTest < Miniswag::TestCase
|
|
121
|
+
openapi_spec "admin.yaml" # target a specific spec file
|
|
122
|
+
|
|
123
|
+
path "/resources/{id}" do
|
|
124
|
+
get "Fetch a resource" do
|
|
125
|
+
response 200, "success" do
|
|
126
|
+
run_test!
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Parameters
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
parameter name: :id, in: :path, type: :integer
|
|
137
|
+
parameter name: :status, in: :query, type: :string
|
|
138
|
+
parameter name: "X-Request-Id", in: :header, type: :string
|
|
139
|
+
parameter name: :body, in: :body, schema: { type: :object, properties: { ... } }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Path parameters are automatically marked `required: true`.
|
|
143
|
+
|
|
144
|
+
### Providing parameter values
|
|
145
|
+
|
|
146
|
+
Instead of rswag's `let` blocks, use `params`:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
response 200, "success" do
|
|
150
|
+
params { { id: @resource.id, status: "active" } }
|
|
151
|
+
run_test!
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Setup (replacing `let!` / `before`)
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
response 200, "success" do
|
|
159
|
+
before { @resource = create_resource(name: "test") }
|
|
160
|
+
params { { id: @resource.id } }
|
|
161
|
+
run_test!
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Custom assertions after the request
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
run_test! do |response|
|
|
169
|
+
data = JSON.parse(response.body)
|
|
170
|
+
assert_equal "test", data["name"]
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Operation attributes
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
get "Fetch resource" do
|
|
178
|
+
tags "Resources"
|
|
179
|
+
operationId "getResource"
|
|
180
|
+
description "Returns a single resource by ID"
|
|
181
|
+
consumes "application/json"
|
|
182
|
+
produces "application/json"
|
|
183
|
+
security [{ bearer: [] }]
|
|
184
|
+
deprecated true
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Response schema & headers
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
response 200, "success" do
|
|
192
|
+
schema type: :object, properties: { id: { type: :integer } }
|
|
193
|
+
header "X-Rate-Limit", type: :integer, description: "Requests per hour"
|
|
194
|
+
run_test!
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Request body examples
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
post "Create resource" do
|
|
202
|
+
request_body_example value: { name: "example" }, summary: "Basic example"
|
|
203
|
+
# ...
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Metadata (custom extensions)
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
response 200, "success" do
|
|
211
|
+
metadata[:operation] ||= {}
|
|
212
|
+
metadata[:operation]["x-public-docs"] = true
|
|
213
|
+
run_test!
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Migrating from rswag
|
|
218
|
+
|
|
219
|
+
| rswag (RSpec) | miniswag (Minitest) |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `RSpec.describe "...", type: :request do` | `class MyTest < Miniswag::TestCase` |
|
|
222
|
+
| `let(:Authorization) { "Bearer ..." }` | `params { { Authorization: "Bearer ..." } }` |
|
|
223
|
+
| `let!(:resource) { create(:resource) }` | `before { @resource = create(:resource) }` |
|
|
224
|
+
| `let(:id) { resource.id }` | include in `params` block |
|
|
225
|
+
| `let(:body) { { name: "x" } }` | include in `params` block |
|
|
226
|
+
| `openapi_spec:` metadata on describe | `openapi_spec "name.yaml"` at class level |
|
|
227
|
+
| `require "swagger_helper"` | `require "openapi_helper"` |
|
|
228
|
+
|
|
229
|
+
## Configuration
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
Miniswag.configure do |config|
|
|
233
|
+
# Required — where to write generated spec files
|
|
234
|
+
config.openapi_root = Rails.root.join("docs/api").to_s
|
|
235
|
+
|
|
236
|
+
# Required — spec definitions (supports multiple files)
|
|
237
|
+
config.openapi_specs = { "v1.yaml" => { openapi: "3.0.1", info: { ... } } }
|
|
238
|
+
|
|
239
|
+
# Optional — output format (:json or :yaml, default :yaml)
|
|
240
|
+
config.openapi_format = :yaml
|
|
241
|
+
|
|
242
|
+
# Optional — strict schema validation (rejects additional properties)
|
|
243
|
+
config.openapi_strict_schema_validation = false
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Requirements
|
|
248
|
+
|
|
249
|
+
- Ruby >= 3.1
|
|
250
|
+
- Rails >= 7.0, < 9.0
|
|
251
|
+
- Minitest ~> 5.0
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT. See [MIT-LICENSE](MIT-LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module Miniswag
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
|
8
|
+
|
|
9
|
+
def copy_openapi_helper
|
|
10
|
+
template('openapi_helper.rb', 'test/openapi_helper.rb')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'miniswag'
|
|
5
|
+
|
|
6
|
+
Miniswag.configure do |config|
|
|
7
|
+
# Root folder where OpenAPI spec files will be generated
|
|
8
|
+
config.openapi_root = Rails.root.join('openapi').to_s
|
|
9
|
+
|
|
10
|
+
# Output format: :json or :yaml
|
|
11
|
+
config.openapi_format = :json
|
|
12
|
+
|
|
13
|
+
# Define one or more OpenAPI specs
|
|
14
|
+
config.openapi_specs = {
|
|
15
|
+
'v1/openapi.json' => {
|
|
16
|
+
openapi: '3.0.1',
|
|
17
|
+
info: {
|
|
18
|
+
title: 'API V1',
|
|
19
|
+
version: 'v1'
|
|
20
|
+
},
|
|
21
|
+
paths: {},
|
|
22
|
+
servers: [
|
|
23
|
+
{ url: 'http://localhost:3000' }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Miniswag
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :openapi_root, :openapi_specs, :openapi_format,
|
|
6
|
+
:openapi_all_properties_required, :openapi_no_additional_properties,
|
|
7
|
+
:dry_run
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@openapi_root = nil
|
|
11
|
+
@openapi_specs = {}
|
|
12
|
+
@openapi_format = :json
|
|
13
|
+
@openapi_all_properties_required = false
|
|
14
|
+
@openapi_no_additional_properties = false
|
|
15
|
+
@dry_run = ENV.key?('MINISWAG_DRY_RUN') ? ENV['MINISWAG_DRY_RUN'] == '1' : true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get_openapi_spec(name)
|
|
19
|
+
return openapi_specs.values.first if name.nil?
|
|
20
|
+
raise ConfigurationError, "Unknown openapi_spec '#{name}'" unless openapi_specs[name]
|
|
21
|
+
|
|
22
|
+
openapi_specs[name]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate!
|
|
26
|
+
raise ConfigurationError, 'No openapi_root provided. See openapi_helper.rb' if openapi_root.nil?
|
|
27
|
+
if openapi_specs.nil? || openapi_specs.empty?
|
|
28
|
+
raise ConfigurationError, 'No openapi_specs defined. See openapi_helper.rb'
|
|
29
|
+
end
|
|
30
|
+
return if %i[json yaml].include?(openapi_format)
|
|
31
|
+
|
|
32
|
+
raise ConfigurationError, "Unknown openapi_format '#{openapi_format}'"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ConfigurationError < StandardError; end
|
|
37
|
+
end
|
data/lib/miniswag/dsl.rb
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
5
|
+
|
|
6
|
+
module Miniswag
|
|
7
|
+
# Class-level DSL methods that mirror rswag's ExampleGroupHelpers.
|
|
8
|
+
#
|
|
9
|
+
# The DSL builds a tree of metadata on the test class. Each `path` block
|
|
10
|
+
# creates a path context, each verb block creates an operation context,
|
|
11
|
+
# and each `response` block creates a response context. `run_test!`
|
|
12
|
+
# generates a Minitest test method from the accumulated metadata.
|
|
13
|
+
#
|
|
14
|
+
# Metadata is stored in class instance variables and accumulated via
|
|
15
|
+
# a context stack so nested blocks see their parent metadata.
|
|
16
|
+
module DSL
|
|
17
|
+
def self.extended(base)
|
|
18
|
+
base.instance_variable_set(:@_miniswag_context_stack, [])
|
|
19
|
+
base.instance_variable_set(:@_miniswag_test_definitions, [])
|
|
20
|
+
base.instance_variable_set(:@_miniswag_openapi_spec_name, nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Set which openapi spec file this test class targets (e.g. "admin.yaml")
|
|
24
|
+
def openapi_spec(name)
|
|
25
|
+
@_miniswag_openapi_spec_name = name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ── Path block ──────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def path(template, &block)
|
|
31
|
+
ctx = { path_item: { template: template, parameters: [] }, scope: :path }
|
|
32
|
+
push_context(ctx, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ── HTTP verb blocks ────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
%i[get post patch put delete head options trace].each do |verb|
|
|
38
|
+
define_method(verb) do |summary, &block|
|
|
39
|
+
ctx = {
|
|
40
|
+
operation: {
|
|
41
|
+
verb: verb,
|
|
42
|
+
summary: summary,
|
|
43
|
+
parameters: []
|
|
44
|
+
},
|
|
45
|
+
scope: :operation
|
|
46
|
+
}
|
|
47
|
+
push_context(ctx, &block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ── Operation-level attributes ──────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
%i[operationId deprecated security].each do |attr_name|
|
|
54
|
+
define_method(attr_name) do |value|
|
|
55
|
+
current_operation[attr_name] = value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def description(value)
|
|
60
|
+
current_operation[:description] = value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
%i[tags consumes produces schemes].each do |attr_name|
|
|
64
|
+
define_method(attr_name) do |*value|
|
|
65
|
+
current_operation[attr_name] = value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ── Parameters ──────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def parameter(attributes)
|
|
72
|
+
attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path
|
|
73
|
+
scope = current_scope
|
|
74
|
+
if scope == :operation
|
|
75
|
+
current_operation[:parameters] ||= []
|
|
76
|
+
current_operation[:parameters] << attributes
|
|
77
|
+
else
|
|
78
|
+
current_path_item[:parameters] ||= []
|
|
79
|
+
current_path_item[:parameters] << attributes
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def request_body_example(value:, summary: nil, name: nil)
|
|
84
|
+
return unless current_scope == :operation
|
|
85
|
+
|
|
86
|
+
current_operation[:request_examples] ||= []
|
|
87
|
+
example_entry = { value: value }
|
|
88
|
+
example_entry[:summary] = summary if summary
|
|
89
|
+
example_entry[:name] = name || current_operation[:request_examples].length
|
|
90
|
+
current_operation[:request_examples] << example_entry
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ── Response block ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def response(code, description, &block)
|
|
96
|
+
ctx = {
|
|
97
|
+
response: { code: code, description: description },
|
|
98
|
+
scope: :response,
|
|
99
|
+
before_blocks: [],
|
|
100
|
+
params_block: nil,
|
|
101
|
+
after_test_block: nil
|
|
102
|
+
}
|
|
103
|
+
push_context(ctx, &block)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ── Response-level attributes ───────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def schema(value)
|
|
109
|
+
current_response[:schema] = value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def header(name, attributes)
|
|
113
|
+
current_response[:headers] ||= {}
|
|
114
|
+
current_response[:headers][name] = attributes
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def examples(examples_hash = nil)
|
|
118
|
+
return if examples_hash.nil?
|
|
119
|
+
|
|
120
|
+
examples_hash.each_with_index do |(mime, example_object), index|
|
|
121
|
+
example(mime, "example_#{index}", example_object)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def example(mime, name, value, summary = nil, description = nil)
|
|
126
|
+
current_response[:content] = {} if current_response[:content].blank?
|
|
127
|
+
if current_response[:content][mime].blank?
|
|
128
|
+
current_response[:content][mime] = {}
|
|
129
|
+
current_response[:content][mime][:examples] = {}
|
|
130
|
+
end
|
|
131
|
+
example_object = { value: value, summary: summary, description: description }.compact
|
|
132
|
+
current_response[:content][mime][:examples].merge!(name.to_sym => example_object)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ── Metadata access (for direct manipulation like metadata[:operation]["x-public-docs"]) ─
|
|
136
|
+
|
|
137
|
+
def metadata
|
|
138
|
+
@_miniswag_context_stack.last || {}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ── Setup blocks within response context ────────────────────────────
|
|
142
|
+
|
|
143
|
+
# Register a block to run before the test request (within response context).
|
|
144
|
+
# Replaces RSpec's `let!` blocks for test data setup.
|
|
145
|
+
def before(&block)
|
|
146
|
+
ctx = @_miniswag_context_stack.last
|
|
147
|
+
ctx[:before_blocks] << block if ctx && ctx.key?(:before_blocks)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Register a block that returns a hash of parameter values.
|
|
151
|
+
# Keys should match parameter names (including Authorization, path params, etc.)
|
|
152
|
+
def params(&block)
|
|
153
|
+
ctx = @_miniswag_context_stack.last
|
|
154
|
+
ctx[:params_block] = block if ctx
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ── Test generation ─────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
def run_test!(test_description = nil, &after_block)
|
|
160
|
+
# Snapshot all the accumulated metadata at this point
|
|
161
|
+
path_item = deep_dup(current_path_item)
|
|
162
|
+
operation = deep_dup(current_operation)
|
|
163
|
+
response_meta = deep_dup(current_response)
|
|
164
|
+
openapi_spec_name = @_miniswag_openapi_spec_name
|
|
165
|
+
before_blocks = (@_miniswag_context_stack.last[:before_blocks] || []).dup
|
|
166
|
+
params_block = @_miniswag_context_stack.last[:params_block]
|
|
167
|
+
|
|
168
|
+
test_description ||= "returns a #{response_meta[:code]} response"
|
|
169
|
+
|
|
170
|
+
# Build a unique test name from path + verb + response code + description
|
|
171
|
+
verb = operation[:verb]
|
|
172
|
+
path_template = path_item[:template]
|
|
173
|
+
test_name = "test_#{verb}_#{path_template}_#{response_meta[:code]}_#{test_description}"
|
|
174
|
+
.gsub(/[^a-zA-Z0-9_]/, '_')
|
|
175
|
+
.gsub(/_+/, '_')
|
|
176
|
+
.downcase
|
|
177
|
+
|
|
178
|
+
# Build full metadata hash (mirrors rswag's metadata structure)
|
|
179
|
+
full_metadata = {
|
|
180
|
+
path_item: path_item,
|
|
181
|
+
operation: operation,
|
|
182
|
+
response: response_meta,
|
|
183
|
+
openapi_spec: openapi_spec_name
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Register for OpenAPI generation
|
|
187
|
+
@_miniswag_test_definitions ||= []
|
|
188
|
+
@_miniswag_test_definitions << full_metadata
|
|
189
|
+
|
|
190
|
+
# Register this class with the global registry for OpenAPI generation
|
|
191
|
+
Miniswag.register_test_class(self)
|
|
192
|
+
|
|
193
|
+
# Define the actual Minitest test method
|
|
194
|
+
user_block = after_block
|
|
195
|
+
captured_before_blocks = before_blocks
|
|
196
|
+
captured_params_block = params_block
|
|
197
|
+
captured_metadata = full_metadata
|
|
198
|
+
|
|
199
|
+
define_method(test_name) do
|
|
200
|
+
# Run before blocks in instance context
|
|
201
|
+
captured_before_blocks.each { |blk| instance_exec(&blk) }
|
|
202
|
+
|
|
203
|
+
# Collect params from the params block
|
|
204
|
+
test_params = captured_params_block ? instance_exec(&captured_params_block) : {}
|
|
205
|
+
test_params ||= {}
|
|
206
|
+
|
|
207
|
+
# Merge instance variable @_miniswag_params if set (from setup blocks)
|
|
208
|
+
if defined?(@_miniswag_params) && @_miniswag_params.is_a?(Hash)
|
|
209
|
+
test_params = @_miniswag_params.merge(test_params)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build and send request
|
|
213
|
+
factory = Miniswag::RequestFactory.new(captured_metadata, test_params)
|
|
214
|
+
request = factory.build_request
|
|
215
|
+
|
|
216
|
+
send(
|
|
217
|
+
request[:verb],
|
|
218
|
+
request[:path],
|
|
219
|
+
params: request[:payload],
|
|
220
|
+
headers: request[:headers]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Validate response
|
|
224
|
+
validator = Miniswag::ResponseValidator.new
|
|
225
|
+
validator.validate!(captured_metadata, response)
|
|
226
|
+
|
|
227
|
+
# Run user's additional assertions
|
|
228
|
+
instance_exec(response, &user_block) if user_block
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def push_context(ctx, &block)
|
|
235
|
+
@_miniswag_context_stack.push(ctx)
|
|
236
|
+
instance_exec(&block) if block
|
|
237
|
+
@_miniswag_context_stack.pop
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def current_path_item
|
|
241
|
+
frame = @_miniswag_context_stack.find { |c| c[:scope] == :path }
|
|
242
|
+
frame ? frame[:path_item] : {}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def current_operation
|
|
246
|
+
frame = @_miniswag_context_stack.reverse.find { |c| c[:scope] == :operation }
|
|
247
|
+
frame ? frame[:operation] : {}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def current_response
|
|
251
|
+
frame = @_miniswag_context_stack.reverse.find { |c| c[:scope] == :response }
|
|
252
|
+
frame ? frame[:response] : {}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def current_scope
|
|
256
|
+
frame = @_miniswag_context_stack.last
|
|
257
|
+
frame ? frame[:scope] : nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def deep_dup(obj)
|
|
261
|
+
case obj
|
|
262
|
+
when Hash
|
|
263
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
264
|
+
when Array
|
|
265
|
+
obj.map { |v| deep_dup(v) }
|
|
266
|
+
else
|
|
267
|
+
obj.duplicable? ? obj.dup : obj
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json-schema'
|
|
4
|
+
|
|
5
|
+
module Miniswag
|
|
6
|
+
class ExtendedSchema < JSON::Schema::Draft4
|
|
7
|
+
def initialize
|
|
8
|
+
super
|
|
9
|
+
@uri = URI.parse('http://tempuri.org/miniswag/extended_schema')
|
|
10
|
+
@names = ['http://tempuri.org/miniswag/extended_schema']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def validate(current_schema, data, *)
|
|
14
|
+
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
JSON::Validator.register_validator(ExtendedSchema.new)
|
|
21
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest'
|
|
4
|
+
|
|
5
|
+
module Minitest
|
|
6
|
+
# Minitest plugin that triggers OpenAPI generation after the test suite.
|
|
7
|
+
# Activated by setting MINISWAG_GENERATE=1 or via the rake task.
|
|
8
|
+
def self.plugin_miniswag_init(options)
|
|
9
|
+
return unless ENV['MINISWAG_GENERATE'] == '1'
|
|
10
|
+
|
|
11
|
+
reporter << Miniswag::Reporter.new(options[:io], options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.plugin_miniswag_options(opts, _options)
|
|
15
|
+
opts.on '--miniswag-generate', 'Generate OpenAPI specs after test run' do
|
|
16
|
+
ENV['MINISWAG_GENERATE'] = '1'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module Miniswag
|
|
22
|
+
class Reporter < Minitest::StatisticsReporter
|
|
23
|
+
def report
|
|
24
|
+
super
|
|
25
|
+
return if errors > 0 || failures > 0
|
|
26
|
+
|
|
27
|
+
puts 'Miniswag: Generating OpenAPI specs...'
|
|
28
|
+
require 'miniswag/openapi_generator'
|
|
29
|
+
generator = Miniswag::OpenapiGenerator.new
|
|
30
|
+
generator.generate!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|