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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
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
@@ -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