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.
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/deep_merge'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+
8
+ module Miniswag
9
+ # Collects metadata from test definitions and generates OpenAPI spec files.
10
+ # This is the Minitest equivalent of rswag's OpenapiFormatter.
11
+ class OpenapiGenerator
12
+ def initialize(config = Miniswag.config)
13
+ @config = config
14
+ end
15
+
16
+ def generate!
17
+ @config.validate!
18
+
19
+ # Process all registered test class metadata
20
+ Miniswag.registered_test_classes.each do |test_class|
21
+ test_class.miniswag_test_definitions.each do |metadata|
22
+ next if metadata[:document] == false
23
+ next unless metadata.key?(:response)
24
+
25
+ openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec])
26
+ raise ConfigurationError, 'Unsupported OpenAPI version' unless doc_version(openapi_spec)&.start_with?('3')
27
+
28
+ upgrade_request_type!(metadata)
29
+ upgrade_response_produces!(openapi_spec, metadata)
30
+ openapi_spec.deep_merge!(metadata_to_openapi(metadata))
31
+ end
32
+ end
33
+
34
+ # Post-process and write files
35
+ @config.openapi_specs.each do |url_path, doc|
36
+ parse_parameters(doc)
37
+ file_path = File.join(@config.openapi_root, url_path)
38
+ dirname = File.dirname(file_path)
39
+ FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
40
+ File.open(file_path, 'w') do |file|
41
+ file.write(pretty_generate(doc))
42
+ end
43
+ puts "Miniswag: OpenAPI doc generated at #{file_path}"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def doc_version(doc)
50
+ doc[:openapi]
51
+ end
52
+
53
+ def pretty_generate(doc)
54
+ if @config.openapi_format == :yaml
55
+ clean_doc = JSON.parse(JSON.pretty_generate(doc))
56
+ YAML.dump(clean_doc)
57
+ else
58
+ JSON.pretty_generate(doc)
59
+ end
60
+ end
61
+
62
+ def metadata_to_openapi(metadata)
63
+ response_code = metadata[:response][:code]
64
+ response = metadata[:response].reject { |k, _v| k == :code }
65
+ verb = metadata[:operation][:verb]
66
+ operation = metadata[:operation]
67
+ .reject { |k, _v| k == :verb }
68
+ .merge(responses: { response_code => response })
69
+ path_template = metadata[:path_item][:template]
70
+ path_item = metadata[:path_item]
71
+ .reject { |k, _v| k == :template }
72
+ .merge(verb => operation)
73
+ { paths: { path_template => path_item } }
74
+ end
75
+
76
+ def upgrade_response_produces!(openapi_spec, metadata)
77
+ mime_list = Array(metadata[:operation][:produces] || openapi_spec[:produces])
78
+ target_node = metadata[:response]
79
+ upgrade_content!(mime_list, target_node)
80
+ metadata[:response].delete(:schema)
81
+ end
82
+
83
+ def upgrade_content!(mime_list, target_node)
84
+ schema = target_node[:schema]
85
+ return if mime_list.empty? || schema.nil?
86
+
87
+ target_node[:content] ||= {}
88
+ mime_list.each do |mime_type|
89
+ (target_node[:content][mime_type] ||= {}).merge!(schema: schema)
90
+ end
91
+ end
92
+
93
+ def upgrade_request_type!(metadata)
94
+ operation_nodes = metadata[:operation][:parameters] || []
95
+ path_nodes = metadata[:path_item][:parameters] || []
96
+ header_node = metadata[:response][:headers] || {}
97
+ (operation_nodes + path_nodes + header_node.values).each do |node|
98
+ if node && node[:type] && node[:schema].nil?
99
+ node[:schema] = { type: node[:type] }
100
+ node.delete(:type)
101
+ end
102
+ end
103
+ end
104
+
105
+ def remove_invalid_operation_keys!(value)
106
+ return unless value.is_a?(Hash)
107
+
108
+ value.delete(:consumes) if value[:consumes]
109
+ value.delete(:produces) if value[:produces]
110
+ value.delete(:request_examples) if value[:request_examples]
111
+ end
112
+
113
+ def parse_parameters(doc)
114
+ doc[:paths]&.each_pair do |_k, path|
115
+ path.each_pair do |_verb, endpoint|
116
+ is_hash = endpoint.is_a?(Hash)
117
+ if is_hash && endpoint[:parameters]
118
+ mime_list = endpoint[:consumes] || doc[:consumes]
119
+ parse_endpoint(endpoint, mime_list)
120
+ end
121
+ remove_invalid_operation_keys!(endpoint)
122
+ end
123
+ end
124
+ end
125
+
126
+ def parse_endpoint(endpoint, mime_list)
127
+ parameters = endpoint[:parameters]
128
+ parameters.each do |parameter|
129
+ set_parameter_schema(parameter)
130
+ convert_file_parameter(parameter)
131
+ parse_enum(parameter)
132
+ end
133
+ parameters.select { |p| parameter_in_form_data_or_body?(p) }.each do |parameter|
134
+ parse_form_data_or_body_parameter(endpoint, parameter, mime_list)
135
+ parameters.delete(parameter)
136
+ end
137
+ parameters.each { |p| p.delete(:schema) if p[:schema].blank? }
138
+ end
139
+
140
+ def set_parameter_schema(parameter)
141
+ parameter[:schema] ||= {}
142
+ if parameter[:schema].key?(:required) && parameter[:schema][:required] == true
143
+ parameter[:required] = parameter[:schema].delete(:required)
144
+ end
145
+ parameter[:schema][:type] = parameter.delete(:type) if parameter.key?(:type)
146
+ end
147
+
148
+ def parameter_in_form_data_or_body?(p)
149
+ p[:in] == :formData || parameter_in_body?(p)
150
+ end
151
+
152
+ def parameter_in_body?(p)
153
+ p[:in] == :body
154
+ end
155
+
156
+ def parse_form_data_or_body_parameter(endpoint, parameter, mime_list)
157
+ unless mime_list
158
+ raise ConfigurationError,
159
+ 'A body or form data parameters are specified without a Media Type for the content'
160
+ end
161
+ add_request_body(endpoint)
162
+ desc = parameter.delete(:description)
163
+ parameter[:schema][:description] = desc if desc
164
+ mime_list.each do |mime|
165
+ endpoint[:requestBody][:content][mime] ||= {}
166
+ mime_config = endpoint[:requestBody][:content][mime]
167
+ next unless mime_config[:schema].nil? || mime_config.dig(:schema, :properties)
168
+
169
+ set_mime_config(mime_config, parameter)
170
+ set_mime_examples(mime_config, endpoint)
171
+ set_request_body_required(mime_config, endpoint, parameter)
172
+ end
173
+ end
174
+
175
+ def add_request_body(endpoint)
176
+ return if endpoint.dig(:requestBody, :content)
177
+
178
+ endpoint[:requestBody] = { content: {} }
179
+ end
180
+
181
+ def set_request_body_required(mime_config, endpoint, parameter)
182
+ return unless parameter[:required]
183
+
184
+ endpoint[:requestBody][:required] = true
185
+ return if parameter_in_body?(parameter)
186
+
187
+ if parameter[:name]
188
+ mime_config[:schema][:required] ||= []
189
+ mime_config[:schema][:required] << parameter[:name].to_s
190
+ else
191
+ mime_config[:schema][:required] = true
192
+ end
193
+ end
194
+
195
+ def convert_file_parameter(parameter)
196
+ return unless parameter[:schema][:type] == :file
197
+
198
+ parameter[:schema][:type] = :string
199
+ parameter[:schema][:format] = :binary
200
+ end
201
+
202
+ def set_mime_config(mime_config, parameter)
203
+ schema_with_form_properties = parameter[:name] && !parameter_in_body?(parameter)
204
+ mime_config[:schema] ||= schema_with_form_properties ? { type: :object, properties: {} } : parameter[:schema]
205
+ return unless schema_with_form_properties
206
+
207
+ mime_config[:schema][:properties][parameter[:name]] = parameter[:schema]
208
+ set_mime_encoding(mime_config, parameter)
209
+ end
210
+
211
+ def set_mime_encoding(mime_config, parameter)
212
+ return unless parameter[:encoding]
213
+
214
+ encoding = parameter[:encoding].dup
215
+ encoding[:contentType] = encoding[:contentType].join(',') if encoding[:contentType].is_a?(Array)
216
+ mime_config[:encoding] ||= {}
217
+ mime_config[:encoding][parameter[:name]] = encoding
218
+ end
219
+
220
+ def set_mime_examples(mime_config, endpoint)
221
+ examples = endpoint[:request_examples]
222
+ return unless examples
223
+
224
+ examples.each do |ex|
225
+ mime_config[:examples] ||= {}
226
+ mime_config[:examples][ex[:name]] = {
227
+ summary: ex[:summary] || endpoint[:summary],
228
+ value: ex[:value]
229
+ }
230
+ end
231
+ end
232
+
233
+ def parse_enum(parameter)
234
+ return unless parameter.key?(:enum)
235
+
236
+ enum = parameter.delete(:enum)
237
+ parameter[:schema] ||= {}
238
+ parameter[:schema][:enum] = enum.is_a?(Hash) ? enum.keys.map(&:to_s) : enum
239
+ parameter[:description] = generate_enum_description(parameter, enum) if enum.is_a?(Hash)
240
+ end
241
+
242
+ def generate_enum_description(param, enum)
243
+ enum_description = "#{param[:description]}:\n "
244
+ enum.each do |k, v|
245
+ enum_description += "* `#{k}` #{v}\n "
246
+ end
247
+ enum_description
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Miniswag
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path('../tasks/miniswag_tasks.rake', __dir__)
7
+ end
8
+
9
+ generators do
10
+ require 'generators/miniswag/install/install_generator'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/hash/slice'
5
+ require 'active_support/core_ext/hash/conversions'
6
+ require 'json'
7
+ require 'cgi'
8
+
9
+ module Miniswag
10
+ class RequestFactory
11
+ attr_accessor :metadata, :params, :headers
12
+
13
+ def initialize(metadata, params = {}, headers = {}, config = Miniswag.config)
14
+ @config = config
15
+ @metadata = metadata
16
+ @params = params.transform_keys(&:to_s)
17
+ @headers = headers.transform_keys(&:to_s)
18
+ end
19
+
20
+ def build_request
21
+ openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec])
22
+ parameters = expand_parameters(metadata, openapi_spec)
23
+ {}.tap do |request|
24
+ add_verb(request, metadata)
25
+ add_path(request, metadata, openapi_spec, parameters)
26
+ add_headers(request, metadata, openapi_spec, parameters)
27
+ add_payload(request, parameters)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def expand_parameters(metadata, openapi_spec)
34
+ operation_params = metadata[:operation][:parameters] || []
35
+ path_item_params = metadata[:path_item][:parameters] || []
36
+ security_params = derive_security_params(metadata, openapi_spec)
37
+ (operation_params + path_item_params + security_params)
38
+ .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], openapi_spec) : p }
39
+ .uniq { |p| p[:name] }
40
+ .reject do |p|
41
+ p[:required] == false &&
42
+ !@headers.key?(p[:name].to_s) &&
43
+ !@params.key?(p[:name].to_s)
44
+ end
45
+ end
46
+
47
+ def derive_security_params(metadata, openapi_spec)
48
+ requirements = metadata[:operation][:security] || openapi_spec[:security] || []
49
+ scheme_names = requirements.flat_map(&:keys)
50
+ schemes = security_version(scheme_names, openapi_spec)
51
+ schemes.map do |scheme|
52
+ param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
53
+ param.merge(schema: { type: :string }, required: requirements.one?)
54
+ end
55
+ end
56
+
57
+ def security_version(scheme_names, openapi_spec)
58
+ components = openapi_spec[:components] || {}
59
+ (components[:securitySchemes] || {}).slice(*scheme_names).values
60
+ end
61
+
62
+ def resolve_parameter(ref, openapi_spec)
63
+ key = ref.sub('#/components/parameters/', '').to_sym
64
+ definitions = (openapi_spec[:components] || {})[:parameters]
65
+ raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
66
+
67
+ definitions[key]
68
+ end
69
+
70
+ def add_verb(request, metadata)
71
+ request[:verb] = metadata[:operation][:verb]
72
+ end
73
+
74
+ def base_path_from_servers(openapi_spec, use_server = :default)
75
+ return '' if openapi_spec[:servers].nil? || openapi_spec[:servers].empty?
76
+
77
+ server = openapi_spec[:servers].first
78
+ variables = {}
79
+ server.fetch(:variables, {}).each_pair { |k, v| variables[k] = v[use_server] }
80
+ base_path = server[:url].gsub(/\{(.*?)\}/) { variables[::Regexp.last_match(1).to_sym] }
81
+ URI(base_path).path
82
+ end
83
+
84
+ def add_path(request, metadata, openapi_spec, parameters)
85
+ template = base_path_from_servers(openapi_spec) + metadata[:path_item][:template]
86
+ request[:path] = template.tap do |path_template|
87
+ parameters.select { |p| p[:in] == :path }.each do |p|
88
+ param_value = @params.fetch(p[:name].to_s) do
89
+ raise ArgumentError, "`#{p[:name]}` parameter key present in path but not provided in params"
90
+ end
91
+ path_template.gsub!("{#{p[:name]}}", param_value.to_s)
92
+ end
93
+ parameters.select { |p| p[:in] == :query && @params.key?(p[:name].to_s) }.each_with_index do |p, i|
94
+ path_template.concat(i.zero? ? '?' : '&')
95
+ path_template.concat(build_query_string_part(p, @params.fetch(p[:name].to_s), openapi_spec))
96
+ end
97
+ end
98
+ end
99
+
100
+ def build_query_string_part(param, value, _openapi_spec)
101
+ name = param[:name]
102
+ escaped_name = CGI.escape(name.to_s)
103
+ return unless param[:schema]
104
+
105
+ style = param[:style]&.to_sym || :form
106
+ explode = param[:explode].nil? || param[:explode]
107
+ type = param.dig(:schema, :type)&.to_sym
108
+ case type
109
+ when :object
110
+ case style
111
+ when :deepObject then { name => value }.to_query
112
+ when :form
113
+ return value.to_query if explode
114
+
115
+ "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(',')
116
+ end
117
+ when :array
118
+ case explode
119
+ when true
120
+ value.to_a.flatten.map { |v| "#{escaped_name}=#{CGI.escape(v.to_s)}" }.join('&')
121
+ else
122
+ separator = case style
123
+ when :form then ','
124
+ when :spaceDelimited then '%20'
125
+ when :pipeDelimited then '|'
126
+ end
127
+ "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(separator)
128
+ end
129
+ else
130
+ "#{escaped_name}=#{CGI.escape(value.to_s)}"
131
+ end
132
+ end
133
+
134
+ def add_headers(request, metadata, openapi_spec, parameters)
135
+ tuples = parameters
136
+ .select { |p| p[:in] == :header }
137
+ .map { |p| [p[:name], @headers.fetch(p[:name].to_s, '').to_s] }
138
+
139
+ produces = metadata[:operation][:produces] || openapi_spec[:produces]
140
+ if produces
141
+ accept = @headers.fetch('Accept', produces.first)
142
+ tuples << ['Accept', accept]
143
+ end
144
+
145
+ consumes = metadata[:operation][:consumes] || openapi_spec[:consumes]
146
+ if consumes
147
+ content_type = @headers.fetch('Content-Type', consumes.first)
148
+ tuples << ['Content-Type', content_type]
149
+ end
150
+
151
+ host = metadata[:operation][:host] || openapi_spec[:host]
152
+ tuples << ['Host', host] if host.present?
153
+
154
+ rack_formatted_tuples = tuples.map do |pair|
155
+ [
156
+ case pair[0]
157
+ when 'Accept' then 'HTTP_ACCEPT'
158
+ when 'Content-Type' then 'CONTENT_TYPE'
159
+ when 'Authorization' then 'HTTP_AUTHORIZATION'
160
+ when 'Host' then 'HTTP_HOST'
161
+ else pair[0]
162
+ end,
163
+ pair[1]
164
+ ]
165
+ end
166
+ request[:headers] = Hash[rack_formatted_tuples]
167
+ end
168
+
169
+ def add_payload(request, parameters)
170
+ content_type = request[:headers]['CONTENT_TYPE']
171
+ return if content_type.nil?
172
+
173
+ request[:payload] = if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
174
+ build_form_payload(parameters)
175
+ elsif %r{\Aapplication/([0-9A-Za-z._-]+\+json\z|json\z)}.match?(content_type)
176
+ build_json_payload(parameters)
177
+ else
178
+ build_raw_payload(parameters)
179
+ end
180
+ end
181
+
182
+ def build_form_payload(parameters)
183
+ tuples = parameters
184
+ .select { |p| p[:in] == :formData }
185
+ .map { |p| [p[:name], @params.fetch(p[:name].to_s)] }
186
+ Hash[tuples]
187
+ end
188
+
189
+ def build_raw_payload(parameters)
190
+ body_param = parameters.find { |p| p[:in] == :body }
191
+ return nil unless body_param
192
+
193
+ @params.fetch(body_param[:name].to_s) do
194
+ raise MissingParameterError, body_param[:name]
195
+ end
196
+ end
197
+
198
+ def build_json_payload(parameters)
199
+ build_raw_payload(parameters)&.to_json
200
+ end
201
+ end
202
+
203
+ class MissingParameterError < StandardError
204
+ attr_reader :body_param
205
+
206
+ def initialize(body_param)
207
+ @body_param = body_param
208
+ end
209
+
210
+ def message
211
+ <<~MSG
212
+ Missing parameter '#{body_param}'
213
+ Please check your test. Ensure you provide the parameter in a `params` block.
214
+ MSG
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/enumerable'
4
+ require 'active_support/core_ext/hash/slice'
5
+ require 'json-schema'
6
+ require 'json'
7
+ require 'miniswag/extended_schema'
8
+
9
+ module Miniswag
10
+ class ResponseValidator
11
+ def initialize(config = Miniswag.config)
12
+ @config = config
13
+ end
14
+
15
+ def validate!(metadata, response)
16
+ openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec])
17
+ validate_code!(metadata, response)
18
+ validate_headers!(metadata, response.headers)
19
+ validate_body!(metadata, openapi_spec, response.body)
20
+ end
21
+
22
+ private
23
+
24
+ def validate_code!(metadata, response)
25
+ expected = metadata[:response][:code].to_s
26
+ return unless response.code != expected
27
+
28
+ raise UnexpectedResponse,
29
+ "Expected response code '#{response.code}' to match '#{expected}'\n" \
30
+ "Response body: #{response.body}"
31
+ end
32
+
33
+ def validate_headers!(metadata, headers)
34
+ header_schemas = metadata[:response][:headers] || {}
35
+ header_schemas.keys.each do |name|
36
+ nullable_attribute = header_schemas.dig(name.to_s, :schema, :nullable)
37
+ required_attribute = header_schemas.dig(name.to_s, :required)
38
+ is_nullable = nullable_attribute.nil? ? false : nullable_attribute
39
+ is_required = required_attribute.nil? || required_attribute
40
+
41
+ if !headers.include?(name.to_s) && is_required
42
+ raise UnexpectedResponse, "Expected response header #{name} to be present"
43
+ end
44
+
45
+ if headers.include?(name.to_s) && headers[name.to_s].nil? && !is_nullable
46
+ raise UnexpectedResponse, "Expected response header #{name} to not be null"
47
+ end
48
+ end
49
+ end
50
+
51
+ def validate_body!(metadata, openapi_spec, body)
52
+ response_schema = metadata[:response][:schema]
53
+ return if response_schema.nil?
54
+
55
+ schemas = { components: openapi_spec[:components] }
56
+ validation_schema = response_schema
57
+ .merge('$schema' => 'http://tempuri.org/miniswag/extended_schema')
58
+ .merge(schemas)
59
+ validation_options = {
60
+ allPropertiesRequired: metadata.fetch(:openapi_all_properties_required,
61
+ @config.openapi_all_properties_required),
62
+ noAdditionalProperties: metadata.fetch(:openapi_no_additional_properties,
63
+ @config.openapi_no_additional_properties)
64
+ }
65
+ errors = JSON::Validator.fully_validate(validation_schema, body, validation_options)
66
+ return unless errors.any?
67
+
68
+ raise UnexpectedResponse,
69
+ "Expected response body to match schema: #{errors.join("\n")}\n" \
70
+ "Response body: #{JSON.pretty_generate(JSON.parse(body))}"
71
+ end
72
+ end
73
+
74
+ class UnexpectedResponse < StandardError; end
75
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_dispatch/testing/integration'
4
+ require 'miniswag/dsl'
5
+ require 'miniswag/request_factory'
6
+ require 'miniswag/response_validator'
7
+
8
+ module Miniswag
9
+ class TestCase < ActionDispatch::IntegrationTest
10
+ extend DSL
11
+
12
+ class << self
13
+ # Returns all test definitions registered via run_test! for OpenAPI generation
14
+ def miniswag_test_definitions
15
+ @_miniswag_test_definitions || []
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Miniswag
4
+ VERSION = '0.1.0'
5
+ end
data/lib/miniswag.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'miniswag/version'
4
+ require 'miniswag/configuration'
5
+ require 'miniswag/railtie' if defined?(Rails::Railtie)
6
+
7
+ module Miniswag
8
+ class << self
9
+ def configure
10
+ yield(config)
11
+ end
12
+
13
+ def config
14
+ @config ||= Configuration.new
15
+ end
16
+
17
+ # Reset configuration (useful for testing)
18
+ def reset!
19
+ @config = nil
20
+ @registered_test_classes = nil
21
+ end
22
+
23
+ # Registry of test classes that have miniswag test definitions.
24
+ # Used by OpenapiGenerator to collect all metadata.
25
+ def register_test_class(klass)
26
+ registered_test_classes << klass unless registered_test_classes.include?(klass)
27
+ end
28
+
29
+ def registered_test_classes
30
+ @registered_test_classes ||= []
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :miniswag do
4
+ desc 'Generate OpenAPI spec files from Minitest integration tests'
5
+ task swaggerize: :environment do
6
+ pattern = ENV.fetch('PATTERN', 'test/integration/**/*_test.rb')
7
+ additional_opts = ENV.fetch('ADDITIONAL_OPTS', '')
8
+
9
+ # Set the env var so the minitest plugin activates
10
+ ENV['MINISWAG_GENERATE'] = '1'
11
+
12
+ # Require the openapi_helper which configures Miniswag
13
+ helper_path = Rails.root.join('test', 'openapi_helper.rb')
14
+ require helper_path.to_s if File.exist?(helper_path)
15
+
16
+ # Run minitest with the matching pattern
17
+ args = [
18
+ 'bin/rails', 'test',
19
+ *Dir.glob(pattern),
20
+ additional_opts.presence
21
+ ].compact
22
+
23
+ system(*args) || abort('Miniswag: Test run failed')
24
+ end
25
+ end
26
+
27
+ task miniswag: ['miniswag:swaggerize']