rspec-swag 0.1.2

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: 725c8f570fabf5d664224343b2241b840ca69ad177973cdb0fb1c4938318c134
4
+ data.tar.gz: 598a18e4bfe3321f8361668f9340b36607a6cd4a54678489b7e98bd974e6d343
5
+ SHA512:
6
+ metadata.gz: 64d7acf87b824fdb1419f0c12f3dd7015b789a4613b1c7459fedd9191a69bc825a5a30c86aa6b01582705e5583b3a3301bf4404ff28ba56151b3beedf2527b99
7
+ data.tar.gz: a3cded64dbf1e3e2fd87a8c908341210bca6f00d19b8741475aaad2a14a3362149563d7a77984b99ed0fe50919414111c9b8652a3e660db3bad318c04a274280
@@ -0,0 +1,17 @@
1
+ RSpec:
2
+ Language:
3
+ ExampleGroups:
4
+ Regular:
5
+ - path
6
+ - response
7
+ - get
8
+ - post
9
+ - patch
10
+ - put
11
+ - delete
12
+ - head
13
+ - options
14
+ - trace
15
+ Examples:
16
+ Regular:
17
+ - run_test!
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2024 GracefulPotato
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env rake
2
+
3
+ # frozen_string_literal: true
4
+
5
+ begin
6
+ require "bundler/setup"
7
+ rescue LoadError
8
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
9
+ end
10
+ begin
11
+ require "rdoc/task"
12
+ rescue LoadError
13
+ require "rdoc/rdoc"
14
+ require "rake/rdoctask"
15
+ RDoc::Task = Rake::RDocTask
16
+ end
17
+
18
+ RDoc::Task.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = "rdoc"
20
+ rdoc.title = "rspec-swag"
21
+ rdoc.options << "--line-numbers"
22
+ rdoc.rdoc_files.include("lib/**/*.rb")
23
+ end
24
+
25
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Swag
5
+ class Configuration
6
+ def initialize(rspec_config)
7
+ @rspec_config = rspec_config
8
+ end
9
+
10
+ def openapi_root
11
+ @openapi_root ||=
12
+ @rspec_config.openapi_root || raise(ConfigurationError, "No openapi_root provided. See swagger_helper.rb")
13
+ end
14
+
15
+ def openapi_specs
16
+ @openapi_specs ||= begin
17
+ if @rspec_config.openapi_specs.nil? || @rspec_config.openapi_specs.empty?
18
+ raise ConfigurationError, "No openapi_specs defined. See swagger_helper.rb"
19
+ end
20
+
21
+ @rspec_config.openapi_specs
22
+ end
23
+ end
24
+
25
+ def swagger_dry_run
26
+ @swagger_dry_run ||= begin
27
+ @rspec_config.swagger_dry_run = ENV["SWAGGER_DRY_RUN"] == "1" if ENV.key?("SWAGGER_DRY_RUN")
28
+
29
+ @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
30
+ end
31
+ end
32
+
33
+ def openapi_format
34
+ @openapi_format ||= begin
35
+ if @rspec_config.openapi_format.nil? || @rspec_config.openapi_format.empty?
36
+ @rspec_config.openapi_format = :json
37
+ end
38
+
39
+ unless [:json, :yaml].include?(@rspec_config.openapi_format)
40
+ raise ConfigurationError, "Unknown openapi_format '#{@rspec_config.openapi_format}'"
41
+ end
42
+
43
+ @rspec_config.openapi_format
44
+ end
45
+ end
46
+
47
+ def get_openapi_spec(name)
48
+ return openapi_specs.values.first if name.nil?
49
+ raise ConfigurationError, "Unknown openapi_spec '#{name}'" unless openapi_specs[name]
50
+
51
+ openapi_specs[name]
52
+ end
53
+
54
+ def get_openapi_spec_version(name)
55
+ doc = get_openapi_spec(name)
56
+ doc[:openapi] || doc[:swagger]
57
+ end
58
+
59
+ def openapi_strict_schema_validation
60
+ @rspec_config.openapi_strict_schema_validation || false
61
+ end
62
+ end
63
+
64
+ class ConfigurationError < StandardError; end
65
+ end
66
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+
5
+ module RSpec
6
+ module Swag
7
+ module ExampleGroupHelpers
8
+ def path(template, metadata = {}, &block)
9
+ metadata[:path_item] = { template: template }
10
+ describe(template, metadata, &block)
11
+ end
12
+
13
+ [:get, :post, :patch, :put, :delete, :head, :options, :trace].each do |verb|
14
+ define_method(verb) do |summary, **metadata, &block|
15
+ api_metadata = { operation: { verb: verb, summary: summary } }.deep_merge(metadata)
16
+ describe(verb, **api_metadata, &block)
17
+ end
18
+ end
19
+
20
+ [:operationId, :deprecated, :security].each do |attr_name|
21
+ define_method(attr_name) do |value|
22
+ metadata[:operation][attr_name] = value
23
+ end
24
+ end
25
+
26
+ # NOTE: 'description' requires special treatment because ExampleGroup already
27
+ # defines a method with that name. Provide an override that supports the existing
28
+ # functionality while also setting the appropriate metadata if applicable
29
+ def description(value = nil)
30
+ return super() if value.nil?
31
+
32
+ metadata[:operation][:description] = value
33
+ end
34
+
35
+ # These are array properties - note the splat operator
36
+ [:tags, :consumes, :produces, :schemes].each do |attr_name|
37
+ define_method(attr_name) do |*value|
38
+ metadata[:operation][attr_name] = value
39
+ end
40
+ end
41
+
42
+ def parameter(attributes)
43
+ attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path
44
+
45
+ if metadata.key?(:operation)
46
+ metadata[:operation][:parameters] ||= []
47
+ metadata[:operation][:parameters] << attributes
48
+ else
49
+ metadata[:path_item][:parameters] ||= []
50
+ metadata[:path_item][:parameters] << attributes
51
+ end
52
+ end
53
+
54
+ def request_body_example(value:, summary: nil, name: nil)
55
+ return unless metadata.key?(:operation)
56
+
57
+ metadata[:operation][:request_examples] ||= []
58
+ example = { value: value }
59
+ example[:summary] = summary if summary
60
+ # We need the examples to have a unique name for a set of examples,
61
+ # so just make the name the length if one isn't provided.
62
+ example[:name] = name || metadata[:operation][:request_examples].length
63
+ metadata[:operation][:request_examples] << example
64
+ end
65
+
66
+ def response(code, description, metadata = {}, &block)
67
+ metadata[:response] = { code: code, description: description }
68
+ context(description, metadata, &block)
69
+ end
70
+
71
+ def schema(value)
72
+ metadata[:response][:schema] = value
73
+ end
74
+
75
+ def header(name, attributes)
76
+ metadata[:response][:headers] ||= {}
77
+
78
+ metadata[:response][:headers][name] = attributes
79
+ end
80
+
81
+ # NOTE: Similar to 'description', 'examples' need to handle the case when
82
+ # being invoked with no params to avoid overriding 'examples' method of
83
+ # rspec-core ExampleGroup
84
+ def examples(examples = nil)
85
+ return super() if examples.nil?
86
+
87
+ # should we add a deprecation warning?
88
+ examples.each_with_index do |(mime, example_object), index|
89
+ example(mime, "example_#{index}", example_object)
90
+ end
91
+ end
92
+
93
+ def example(mime, name, value, summary = nil, description = nil)
94
+ # Todo - move initialization of metadata somewhere else.
95
+ metadata[:response][:content] = {} if metadata[:response][:content].blank?
96
+
97
+ if metadata[:response][:content][mime].blank?
98
+ metadata[:response][:content][mime] = {}
99
+ metadata[:response][:content][mime][:examples] = {}
100
+ end
101
+
102
+ example_object = {
103
+ value: value,
104
+ summary: summary,
105
+ description: description
106
+ }.select { |_, v| v.present? }
107
+ # TODO, issue a warning if example is being overridden with the same key
108
+ metadata[:response][:content][mime][:examples].merge!(
109
+ { name.to_sym => example_object }
110
+ )
111
+ end
112
+
113
+ #
114
+ # Perform request and assert response matches swagger definitions
115
+ #
116
+ # @param description [String] description of the test
117
+ # @param args [Array] arguments to pass to the `it` method
118
+ # @param options [Hash] options to pass to the `it` method
119
+ # @param &block [Proc] you can make additional assertions within that block
120
+ # @return [void]
121
+ def run_test!(description = nil, *args, **options, &block)
122
+ # swagger metadata value defaults to true
123
+ options[:swagger] = true unless options.key?(:swagger)
124
+
125
+ description ||= "returns a #{metadata[:response][:code]} response"
126
+
127
+ before do |example|
128
+ submit_request(example.metadata)
129
+ end
130
+
131
+ it description, *args, **options do |example|
132
+ assert_response_matches_metadata(example.metadata, &block)
133
+ example.instance_exec(last_response, &block) if block_given?
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/swag/request_factory"
4
+ require "rspec/swag/response_validator"
5
+
6
+ module RSpec
7
+ module Swag
8
+ module ExampleHelpers
9
+ def submit_request(metadata)
10
+ request = RequestFactory.new.build_request(metadata, self)
11
+
12
+ send(
13
+ request[:verb],
14
+ request[:path],
15
+ request[:payload],
16
+ request[:headers]
17
+ )
18
+ end
19
+
20
+ def assert_response_matches_metadata(metadata)
21
+ ResponseValidator.new.validate!(metadata, last_response)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json-schema"
4
+
5
+ module RSpec
6
+ module Swag
7
+ class ExtendedSchema < JSON::Schema::Draft4
8
+ def initialize
9
+ super
10
+ @uri = URI.parse("http://tempuri.org/rswag/specs/extended_schema")
11
+ @names = ["http://tempuri.org/rswag/specs/extended_schema"]
12
+ end
13
+
14
+ def validate(current_schema, data, *)
15
+ if data.nil? && (current_schema.schema["nullable"] == true || current_schema.schema["x-nullable"] == true)
16
+ return
17
+ end
18
+
19
+ super
20
+ end
21
+ end
22
+
23
+ JSON::Validator.register_validator(ExtendedSchema.new)
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RSpec
6
+ module Swag
7
+ class ProjectInitializer
8
+ attr_reader :destination, :source
9
+
10
+ def initialize
11
+ @destination = Dir.pwd
12
+ @source = File.expand_path("templates", __dir__)
13
+ end
14
+
15
+ def run
16
+ copy_template "spec/swagger_helper.rb"
17
+ end
18
+
19
+ private
20
+
21
+ def copy_template(file)
22
+ destination_file = File.join(destination, file)
23
+ return report_exists(file) if File.exist?(destination_file)
24
+
25
+ report_creating(file)
26
+ FileUtils.mkdir_p(File.dirname(destination_file))
27
+ File.open(destination_file, "w") do |f|
28
+ f.write File.read(File.join(source, file))
29
+ end
30
+ end
31
+
32
+ def report_exists(file)
33
+ puts " exist #{file}"
34
+ end
35
+
36
+ def report_creating(file)
37
+ puts " create #{file}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ load "rspec/swag/tasks/rspec_swag_tasks.rake"
@@ -0,0 +1,308 @@
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
+
8
+ module RSpec
9
+ module Swag
10
+ # rubocop:disable Metrics/ClassLength
11
+ class RequestFactory
12
+ def initialize(config = ::RSpec::Swag.config)
13
+ @config = config
14
+ end
15
+
16
+ def build_request(metadata, example)
17
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
18
+ parameters = expand_parameters(metadata, swagger_doc, example)
19
+
20
+ {}.tap do |request|
21
+ add_verb(request, metadata)
22
+ add_path(request, metadata, swagger_doc, parameters, example)
23
+ add_headers(request, metadata, swagger_doc, parameters, example)
24
+ add_payload(request, parameters, example)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def expand_parameters(metadata, swagger_doc, example)
31
+ operation_params = metadata[:operation][:parameters] || []
32
+ path_item_params = metadata[:path_item][:parameters] || []
33
+ security_params = derive_security_params(metadata, swagger_doc)
34
+
35
+ # NOTE: Use of + instead of concat to avoid mutation of the metadata object
36
+ (operation_params + path_item_params + security_params)
37
+ .map { |p| p["$ref"] ? resolve_parameter(p["$ref"], swagger_doc) : p }
38
+ .uniq { |p| p[:name] }
39
+ .reject { |p| p[:required] == false && !example.respond_to?(extract_getter(p)) }
40
+ end
41
+
42
+ def derive_security_params(metadata, swagger_doc)
43
+ requirements = metadata[:operation][:security] || swagger_doc[:security] || []
44
+ scheme_names = requirements.flat_map(&:keys)
45
+ schemes = security_version(scheme_names, swagger_doc)
46
+
47
+ schemes.map do |scheme|
48
+ param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: "Authorization", in: :header }
49
+ param.merge(type: :string, required: requirements.one?)
50
+ end
51
+ end
52
+
53
+ def security_version(scheme_names, swagger_doc)
54
+ if doc_version(swagger_doc).start_with?("2")
55
+ (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
56
+ else # Openapi3
57
+ if swagger_doc.key?(:securityDefinitions)
58
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions is replaced in OpenAPI3! "\
59
+ "Rename to components/securitySchemes (in swagger_helper.rb)")
60
+ swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] }
61
+ swagger_doc.delete(:securityDefinitions)
62
+ end
63
+ components = swagger_doc[:components] || {}
64
+ (components[:securitySchemes] || {}).slice(*scheme_names).values
65
+ end
66
+ end
67
+
68
+ def resolve_parameter(ref, swagger_doc)
69
+ key = key_version(ref, swagger_doc)
70
+ definitions = definition_version(swagger_doc)
71
+ raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
72
+
73
+ definitions[key]
74
+ end
75
+
76
+ def key_version(ref, swagger_doc)
77
+ if doc_version(swagger_doc).start_with?("2")
78
+ ref.sub("#/parameters/", "").to_sym
79
+ elsif ref.start_with?("#/parameters/") # Openapi3
80
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: #/parameters/ refs are replaced in OpenAPI3! " \
81
+ "Rename to #/components/parameters/")
82
+ ref.sub("#/parameters/", "").to_sym
83
+ else
84
+ ref.sub("#/components/parameters/", "").to_sym
85
+ end
86
+ end
87
+
88
+ def definition_version(swagger_doc)
89
+ if doc_version(swagger_doc).start_with?("2")
90
+ swagger_doc[:parameters]
91
+ elsif swagger_doc.key?(:parameters) # Openapi3
92
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: parameters is replaced in OpenAPI3! "\
93
+ "Rename to components/parameters (in swagger_helper.rb)")
94
+ swagger_doc[:parameters]
95
+ else
96
+ components = swagger_doc[:components] || {}
97
+ components[:parameters]
98
+ end
99
+ end
100
+
101
+ def add_verb(request, metadata)
102
+ request[:verb] = metadata[:operation][:verb]
103
+ end
104
+
105
+ def base_path_from_servers(swagger_doc, use_server = :default)
106
+ return "" if swagger_doc[:servers].nil? || swagger_doc[:servers].empty?
107
+
108
+ server = swagger_doc[:servers].first
109
+ variables = {}
110
+ server.fetch(:variables, {}).each_pair { |k, v| variables[k] = v[use_server] }
111
+ base_path = server[:url].gsub(/\{(.*?)\}/) { variables[::Regexp.last_match(1).to_sym] }
112
+ URI(base_path).path
113
+ end
114
+
115
+ # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
116
+ def add_path(request, metadata, swagger_doc, parameters, example)
117
+ open_api_3_doc = doc_version(swagger_doc).start_with?("3")
118
+ uses_base_path = swagger_doc[:basePath].present?
119
+
120
+ if open_api_3_doc && uses_base_path
121
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: basePath is replaced in OpenAPI3! " \
122
+ "Update your swagger_helper.rb")
123
+ end
124
+
125
+ template = if uses_base_path
126
+ (swagger_doc[:basePath] || "") + metadata[:path_item][:template]
127
+ else # OpenAPI 3
128
+ base_path_from_servers(swagger_doc) + metadata[:path_item][:template]
129
+ end
130
+
131
+ request[:path] = template.tap do |path_template|
132
+ parameters.select { |p| p[:in] == :path }.each do |p|
133
+ unless example.respond_to?(extract_getter(p))
134
+ raise ArgumentError, "`#{p[:name]}` parameter key present, but not defined within example group" \
135
+ "(i. e `it` or `let` block)"
136
+ end
137
+ path_template.gsub!("{#{p[:name]}}", example.send(extract_getter(p)).to_s)
138
+ end
139
+
140
+ parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
141
+ path_template.concat(i.zero? ? "?" : "&")
142
+ path_template.concat(build_query_string_part(p, example.send(extract_getter(p)), swagger_doc))
143
+ end
144
+ end
145
+ end
146
+ # rubocop:enable all
147
+
148
+ # rubocop:disable Metrics/BlockNesting,Style/HashLikeCase,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
149
+ def build_query_string_part(param, value, swagger_doc)
150
+ name = param[:name]
151
+ escaped_name = CGI.escape(name.to_s)
152
+
153
+ # OAS 3: https://swagger.io/docs/specification/serialization/
154
+ if swagger_doc && doc_version(swagger_doc).start_with?("3") && param[:schema]
155
+ style = param[:style]&.to_sym || :form
156
+ explode = param[:explode].nil? ? true : param[:explode]
157
+
158
+ case param[:schema][:type]&.to_sym
159
+ when :object
160
+ case style
161
+ when :deepObject
162
+ return { name => value }.to_query
163
+ when :form
164
+ return value.to_query if explode
165
+
166
+ return "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(",")
167
+
168
+ end
169
+ when :array
170
+ case explode
171
+ when true
172
+ return value.to_a.flatten.map { |v| "#{escaped_name}=#{CGI.escape(v.to_s)}" }.join("&")
173
+ else
174
+ separator = case style
175
+ when :form then ","
176
+ when :spaceDelimited then "%20"
177
+ when :pipeDelimited then "|"
178
+ end
179
+ return "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(separator)
180
+ end
181
+ else
182
+ return "#{name}=#{value}"
183
+ end
184
+ end
185
+
186
+ type = param[:type] || param.dig(:schema, :type)
187
+ return "#{escaped_name}=#{CGI.escape(value.to_s)}" unless type&.to_sym == :array
188
+
189
+ case param[:collectionFormat]
190
+ when :ssv
191
+ "#{name}=#{value.join(' ')}"
192
+ when :tsv
193
+ "#{name}=#{value.join('\t')}"
194
+ when :pipes
195
+ "#{name}=#{value.join('|')}"
196
+ when :multi
197
+ value.map { |v| "#{name}=#{v}" }.join("&")
198
+ else
199
+ "#{name}=#{value.join(',')}" # csv is default
200
+ end
201
+ end
202
+ # rubocop:enable all
203
+
204
+ # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
205
+ def add_headers(request, metadata, swagger_doc, parameters, example)
206
+ tuples = parameters
207
+ .select { |p| p[:in] == :header }
208
+ .map { |p| [p[:name], example.send(extract_getter(p)).to_s] }
209
+
210
+ # Accept header
211
+ produces = metadata[:operation][:produces] || swagger_doc[:produces]
212
+ if produces
213
+ accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
214
+ tuples << ["Accept", accept]
215
+ end
216
+
217
+ # Content-Type header
218
+ consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
219
+ if consumes
220
+ content_type = example.respond_to?(:"Content-Type") ? example.send(:"Content-Type") : consumes.first
221
+ tuples << ["Content-Type", content_type]
222
+ end
223
+
224
+ # Host header
225
+ host = metadata[:operation][:host] || swagger_doc[:host]
226
+ if host.present?
227
+ host = example.respond_to?(:Host) ? example.send(:Host) : host
228
+ tuples << ["Host", host]
229
+ end
230
+
231
+ # Rails test infrastructure requires rack-formatted headers
232
+ rack_formatted_tuples = tuples.map do |pair|
233
+ [
234
+ case pair[0]
235
+ when "Accept" then "HTTP_ACCEPT"
236
+ when "Content-Type" then "CONTENT_TYPE"
237
+ when "Authorization" then "HTTP_AUTHORIZATION"
238
+ when "Host" then "HTTP_HOST"
239
+ else pair[0]
240
+ end,
241
+ pair[1]
242
+ ]
243
+ end
244
+
245
+ request[:headers] = Hash[rack_formatted_tuples]
246
+ end
247
+ # rubocop:enable all
248
+
249
+ def add_payload(request, parameters, example)
250
+ content_type = request[:headers]["CONTENT_TYPE"]
251
+ return if content_type.nil?
252
+
253
+ request[:payload] = if ["application/x-www-form-urlencoded", "multipart/form-data"].include?(content_type)
254
+ build_form_payload(parameters, example)
255
+ elsif content_type == "application/json"
256
+ build_json_payload(parameters, example)
257
+ else
258
+ build_raw_payload(parameters, example)
259
+ end
260
+ end
261
+
262
+ def build_form_payload(parameters, example)
263
+ # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
264
+ # Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
265
+ # Rails test infrastructure allows us to send the values directly as a hash
266
+ # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
267
+ tuples = parameters
268
+ .select { |p| p[:in] == :formData }
269
+ .map { |p| [p[:name], example.send(extract_getter(p))] }
270
+ Hash[tuples]
271
+ end
272
+
273
+ def build_raw_payload(parameters, example)
274
+ body_param = parameters.select { |p| p[:in] == :body }.first
275
+ return nil unless body_param
276
+
277
+ raise(MissingParameterError, body_param[:name]) unless example.respond_to?(body_param[:name])
278
+
279
+ example.send(body_param[:name])
280
+ end
281
+
282
+ def build_json_payload(parameters, example)
283
+ build_raw_payload(parameters, example)&.to_json
284
+ end
285
+
286
+ def doc_version(doc)
287
+ doc[:openapi] || doc[:swagger] || "3"
288
+ end
289
+
290
+ def extract_getter(parameter)
291
+ parameter[:getter] || parameter[:name]
292
+ end
293
+ end
294
+
295
+ class MissingParameterError < StandardError
296
+ def message
297
+ <<~MSG
298
+ Missing parameter '#{super}'
299
+
300
+ Please check your spec. It looks like you defined a body parameter,
301
+ but did not declare usage via let. Try adding:
302
+
303
+ let(:#{super}) {}
304
+ MSG
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
4
+ require "json-schema"
5
+ require "json"
6
+ require "rspec/swag/extended_schema"
7
+
8
+ module RSpec
9
+ module Swag
10
+ class ResponseValidator
11
+ def initialize(config = ::RSpec::Swag.config)
12
+ @config = config
13
+ end
14
+
15
+ def validate!(metadata, response)
16
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
17
+
18
+ validate_code!(metadata, response)
19
+ validate_headers!(metadata, response.headers)
20
+ validate_body!(metadata, swagger_doc, response.body)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_code!(metadata, response)
26
+ expected = metadata[:response][:code].to_s
27
+ return unless response.status.to_s != expected
28
+
29
+ raise UnexpectedResponse,
30
+ "Expected response code '#{response.status}' to match '#{expected}'\n" \
31
+ "Response body: #{response.body}"
32
+ end
33
+
34
+ # rubocop:disable Metrics/PerceivedComplexity
35
+ def validate_headers!(metadata, headers)
36
+ header_schemas = metadata[:response][:headers] || {}
37
+ expected = header_schemas.keys
38
+ expected.each do |name|
39
+ nullable_attribute = header_schemas.dig(name.to_s, :schema, :nullable)
40
+ required_attribute = header_schemas.dig(name.to_s, :required)
41
+
42
+ is_nullable = nullable_attribute.nil? ? false : nullable_attribute
43
+ is_required = required_attribute.nil? ? true : required_attribute
44
+
45
+ if !headers.include?(name.to_s) && is_required
46
+ raise UnexpectedResponse, "Expected response header #{name} to be present"
47
+ end
48
+
49
+ if headers.include?(name.to_s) && headers[name.to_s].nil? && !is_nullable
50
+ raise UnexpectedResponse, "Expected response header #{name} to not be null"
51
+ end
52
+ end
53
+ end
54
+ # rubocop:enable Metrics/PerceivedComplexity
55
+
56
+ def validate_body!(metadata, swagger_doc, body)
57
+ response_schema = metadata[:response][:schema]
58
+ return if response_schema.nil?
59
+
60
+ version = @config.get_openapi_spec_version(metadata[:openapi_spec] || metadata[:swagger_doc])
61
+ schemas = definitions_or_component_schemas(swagger_doc, version)
62
+
63
+ validation_schema = response_schema
64
+ .merge("$schema" => "http://tempuri.org/rswag/specs/extended_schema")
65
+ .merge(schemas)
66
+
67
+ validation_options = validation_options_from(metadata)
68
+
69
+ errors = JSON::Validator.fully_validate(validation_schema, body, validation_options)
70
+ return unless errors.any?
71
+
72
+ raise UnexpectedResponse,
73
+ "Expected response body to match schema: #{errors.join("\n")}\n" \
74
+ "Response body: #{JSON.pretty_generate(JSON.parse(body))}"
75
+ end
76
+
77
+ # rubocop:disable Style/DoubleNegation
78
+ def validation_options_from(metadata)
79
+ is_strict = !!metadata.fetch(:openapi_strict_schema_validation, @config.openapi_strict_schema_validation)
80
+
81
+ { strict: is_strict }
82
+ end
83
+ # rubocop:enable Style/DoubleNegation
84
+
85
+ def definitions_or_component_schemas(swagger_doc, version)
86
+ if version.start_with?("2")
87
+ swagger_doc.slice(:definitions)
88
+ elsif swagger_doc.key?(:definitions) # Openapi3
89
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: definitions is replaced in OpenAPI3! "\
90
+ "Rename to components/schemas (in swagger_helper.rb)")
91
+ swagger_doc.slice(:definitions)
92
+ else
93
+ components = swagger_doc[:components] || {}
94
+ { components: components }
95
+ end
96
+ end
97
+ end
98
+
99
+ class UnexpectedResponse < StandardError; end
100
+ end
101
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/deep_merge"
4
+ require "rspec/core/formatters/base_text_formatter"
5
+ require "swagger_helper"
6
+
7
+ module RSpec
8
+ module Swag
9
+ # rubocop:disable Metrics/ClassLength
10
+ class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter
11
+ ::RSpec::Core::Formatters.register self, :example_group_finished, :stop
12
+
13
+ def initialize(output, config = RSpec::Swag.config)
14
+ super(output)
15
+ @config = config
16
+
17
+ @output.puts "Generating Swagger docs ..."
18
+ end
19
+
20
+ def example_group_finished(notification)
21
+ metadata = notification.group.metadata
22
+
23
+ # !metadata[:document] won't work, since nil means we should generate docs.
24
+ return if metadata[:document] == false
25
+ return unless metadata.key?(:response)
26
+
27
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
28
+
29
+ unless doc_version(swagger_doc).start_with?("2")
30
+ # This is called multiple times per file!
31
+ # metadata[:operation] is also re-used between examples within file
32
+ # therefore be careful NOT to modify its content here.
33
+ upgrade_request_type!(metadata)
34
+ upgrade_servers!(swagger_doc)
35
+ upgrade_oauth!(swagger_doc)
36
+ upgrade_response_produces!(swagger_doc, metadata)
37
+ end
38
+
39
+ swagger_doc.deep_merge!(metadata_to_swagger(metadata))
40
+ end
41
+
42
+ # rubocop:disable all
43
+ def stop(_notification = nil)
44
+ @config.openapi_specs.each do |url_path, doc|
45
+ unless doc_version(doc).start_with?("2")
46
+ doc[:paths]&.each_pair do |_k, v|
47
+ v.each_pair do |_verb, value|
48
+ is_hash = value.is_a?(Hash)
49
+ if is_hash && value[:parameters]
50
+ schema_param = value[:parameters]&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
51
+ mime_list = value[:consumes] || doc[:consumes]
52
+
53
+ if value && schema_param && mime_list
54
+ value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content)
55
+ value[:requestBody][:required] = true if schema_param[:required]
56
+ value[:requestBody][:description] = schema_param[:description] if schema_param[:description]
57
+ examples = value[:request_examples]
58
+ mime_list.each do |mime|
59
+ value[:requestBody][:content][mime] = { schema: schema_param[:schema] }
60
+ next unless examples
61
+
62
+ value[:requestBody][:content][mime][:examples] ||= {}
63
+ examples.map do |example|
64
+ value[:requestBody][:content][mime][:examples][example[:name]] = {
65
+ summary: example[:summary] || value[:summary],
66
+ value: example[:value]
67
+ }
68
+ end
69
+ end
70
+ end
71
+
72
+ value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
73
+ end
74
+ remove_invalid_operation_keys!(value)
75
+ end
76
+ end
77
+ end
78
+
79
+ file_path = File.join(@config.openapi_root, url_path)
80
+ dirname = File.dirname(file_path)
81
+ FileUtils.mkdir_p dirname unless File.exist?(dirname)
82
+
83
+ File.open(file_path, "w") do |file|
84
+ file.write(pretty_generate(doc))
85
+ end
86
+
87
+ @output.puts "Swagger doc generated at #{file_path}"
88
+ end
89
+ end
90
+ # rubocop:enable all
91
+
92
+ private
93
+
94
+ def pretty_generate(doc)
95
+ if @config.openapi_format == :yaml
96
+ clean_doc = yaml_prepare(doc)
97
+ YAML.dump(clean_doc)
98
+ else # config errors are thrown in 'def openapi_format', no throw needed here
99
+ JSON.pretty_generate(doc)
100
+ end
101
+ end
102
+
103
+ def yaml_prepare(doc)
104
+ json_doc = JSON.pretty_generate(doc)
105
+ JSON.parse(json_doc)
106
+ end
107
+
108
+ def metadata_to_swagger(metadata)
109
+ response_code = metadata[:response][:code]
110
+ response = metadata[:response].reject { |k, _v| k == :code }
111
+
112
+ verb = metadata[:operation][:verb]
113
+ operation = metadata[:operation]
114
+ .reject { |k, _v| k == :verb }
115
+ .merge(responses: { response_code => response })
116
+
117
+ path_template = metadata[:path_item][:template]
118
+ path_item = metadata[:path_item]
119
+ .reject { |k, _v| k == :template }
120
+ .merge(verb => operation)
121
+
122
+ { paths: { path_template => path_item } }
123
+ end
124
+
125
+ def doc_version(doc)
126
+ doc[:openapi] || doc[:swagger] || "3"
127
+ end
128
+
129
+ def upgrade_response_produces!(swagger_doc, metadata)
130
+ # Accept header
131
+ mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces])
132
+ target_node = metadata[:response]
133
+ upgrade_content!(mime_list, target_node)
134
+ metadata[:response].delete(:schema)
135
+ end
136
+
137
+ def upgrade_content!(mime_list, target_node)
138
+ schema = target_node[:schema]
139
+ return if mime_list.empty? || schema.nil?
140
+
141
+ target_node[:content] ||= {}
142
+ mime_list.each do |mime_type|
143
+ # TODO: upgrade to have content-type specific schema
144
+ (target_node[:content][mime_type] ||= {}).merge!(schema: schema)
145
+ end
146
+ end
147
+
148
+ def upgrade_request_type!(metadata)
149
+ # No deprecation here as it seems valid to allow type as a shorthand
150
+ operation_nodes = metadata[:operation][:parameters] || []
151
+ path_nodes = metadata[:path_item][:parameters] || []
152
+ header_node = metadata[:response][:headers] || {}
153
+
154
+ (operation_nodes + path_nodes + [header_node]).each do |node|
155
+ if node && node[:type] && node[:schema].nil?
156
+ node[:schema] = { type: node[:type] }
157
+ node.delete(:type)
158
+ end
159
+ end
160
+ end
161
+
162
+ def upgrade_servers!(swagger_doc)
163
+ return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
164
+
165
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: schemes, host, and basePath are replaced " \
166
+ "in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)")
167
+
168
+ swagger_doc[:servers] = { urls: [] }
169
+ swagger_doc[:schemes].each do |scheme|
170
+ swagger_doc[:servers][:urls] << "#{scheme}://#{swagger_doc[:host]}#{swagger_doc[:basePath]}"
171
+ end
172
+
173
+ swagger_doc.delete(:schemes)
174
+ swagger_doc.delete(:host)
175
+ swagger_doc.delete(:basePath)
176
+ end
177
+
178
+ # rubocop:disable all
179
+ def upgrade_oauth!(swagger_doc)
180
+ # find flow in securitySchemes (securityDefinitions will have been re-written)
181
+ schemes = swagger_doc.dig(:components, :securitySchemes)
182
+ return unless schemes&.any? { |_k, v| v.key?(:flow) }
183
+
184
+ schemes.each do |name, v|
185
+ next unless v.key?(:flow)
186
+
187
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions flow is replaced in OpenAPI3! " \
188
+ "Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)")
189
+ flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
190
+ if flow == "accessCode"
191
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions accessCode is replaced " \
192
+ "in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)")
193
+ flow = "authorizationCode"
194
+ end
195
+ if flow == "application"
196
+ RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions application is replaced " \
197
+ "in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)")
198
+ flow = "clientCredentials"
199
+ end
200
+ flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
201
+ a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
202
+ end
203
+ swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
204
+ end
205
+ end
206
+ # rubocop:enable all
207
+
208
+ def remove_invalid_operation_keys!(value)
209
+ return unless value.is_a?(Hash)
210
+
211
+ value&.delete(:consumes)
212
+ value&.delete(:produces)
213
+ value&.delete(:request_examples)
214
+ value[:parameters]&.each { |p| p.delete(:getter) }
215
+ end
216
+ end
217
+ # rubocop:enable Metrics/ClassLength
218
+ end
219
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/swag"
4
+ require "rspec/core/rake_task"
5
+ require "rspec/swag/project_initializer"
6
+
7
+ namespace :rspec do
8
+ desc "Generate Swagger JSON files from integration specs"
9
+ RSpec::Core::RakeTask.new("swaggerize") do |t|
10
+ t.pattern = ENV.fetch(
11
+ "PATTERN",
12
+ "spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb"
13
+ )
14
+
15
+ additional_rspec_opts = ENV.fetch(
16
+ "ADDITIONAL_RSPEC_OPTS",
17
+ ""
18
+ )
19
+
20
+ t.rspec_opts = [additional_rspec_opts]
21
+
22
+ t.rspec_opts += if RSpec::Swag.config.swagger_dry_run
23
+ ["--format RSpec::Swag::SwaggerFormatter", "--dry-run", "--order defined"]
24
+ else
25
+ ["--format RSpec::Swag::SwaggerFormatter", "--order defined"]
26
+ end
27
+ end
28
+
29
+ namespace :swag do
30
+ desc "Copy swagger_helper.rb to spec/"
31
+ task :install do
32
+ RSpec::Swag::ProjectInitializer.new.run
33
+ end
34
+ end
35
+ end
36
+
37
+ task swaggerize: ["rspec:swaggerize"]
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "rspec/swag"
5
+
6
+ RSpec.configure do |config|
7
+ # Specify a root folder where Swagger JSON files are generated
8
+ # NOTE: If you're using the rswag-api to serve API descriptions, you'll need
9
+ # to ensure that it's configured to serve Swagger from the same folder
10
+ config.openapi_root = File.expand_path("../", File.dirname(__FILE__))
11
+
12
+ # Define one or more Swagger documents and provide global metadata for each one
13
+ # When you run the 'rspec:swaggerize' rake task, the complete Swagger will
14
+ # be generated at the provided relative path under openapi_root
15
+ # By default, the operations defined in spec files are added to the first
16
+ # document below. You can override this behavior by adding a openapi_spec tag to the
17
+ # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json'
18
+ config.openapi_specs = {
19
+ "v1/swagger.yaml" => {
20
+ openapi: "3.0.1",
21
+ info: {
22
+ title: "API V1",
23
+ version: "v1"
24
+ },
25
+ paths: {},
26
+ servers: [
27
+ {
28
+ url: "{protocol}://{host}:{port}",
29
+ variables: {
30
+ protocol: {
31
+ enum: ["http", "https"],
32
+ default: "http"
33
+ },
34
+ host: {
35
+ default: "localhost"
36
+ },
37
+ port: {
38
+ default: "3000"
39
+ }
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ }
45
+
46
+ # Specify the format of the output Swagger file when running 'rspec:swaggerize'.
47
+ # The openapi_specs configuration option has the filename including format in
48
+ # the key, this may want to be changed to avoid putting yaml in json files.
49
+ # Defaults to json. Accepts ':json' and ':yaml'.
50
+ config.openapi_format = :yaml
51
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Swag
5
+ VERSION = "0.1.2"
6
+ end
7
+ end
data/lib/rspec/swag.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "rspec/swag/example_group_helpers"
5
+ require "rspec/swag/example_helpers"
6
+ require "rspec/swag/configuration"
7
+
8
+ module RSpec
9
+ module Swag
10
+ # Extend RSpec with a swagger-based DSL
11
+ ::RSpec.configure do |c|
12
+ c.add_setting :openapi_root
13
+ c.add_setting :openapi_specs
14
+ c.add_setting :swagger_dry_run
15
+ c.add_setting :openapi_format, default: :json
16
+ c.add_setting :openapi_strict_schema_validation
17
+ c.extend ExampleGroupHelpers
18
+ c.include ExampleHelpers
19
+ end
20
+
21
+ def self.config
22
+ @config ||= Configuration.new(RSpec.configuration)
23
+ end
24
+
25
+ def self.deprecator
26
+ @deprecator ||= ActiveSupport::Deprecation.new("3.0", "rspec-swag")
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-swag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Richie Morris
8
+ - Greg Myers
9
+ - Jay Danielian
10
+ - GracefulPotato
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2024-02-24 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '3.1'
23
+ - - "<"
24
+ - !ruby/object:Gem::Version
25
+ version: '7.2'
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '7.2'
36
+ - !ruby/object:Gem::Dependency
37
+ name: json-schema
38
+ requirement: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '2.2'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '5.0'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '2.2'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '5.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec-core
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '4.0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '3.0'
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: rspec
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 3.13.0
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 3.13.0
90
+ - !ruby/object:Gem::Dependency
91
+ name: climate_control
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.0
97
+ - - "<"
98
+ - !ruby/object:Gem::Version
99
+ version: '2.0'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 1.0.0
107
+ - - "<"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '='
115
+ - !ruby/object:Gem::Version
116
+ version: 1.60.2
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '='
122
+ - !ruby/object:Gem::Version
123
+ version: 1.60.2
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '='
129
+ - !ruby/object:Gem::Version
130
+ version: 0.21.2
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '='
136
+ - !ruby/object:Gem::Version
137
+ version: 0.21.2
138
+ description: 'Simplify API integration testing with a succinct rspec DSL and generate
139
+ OpenAPI specification files directly from your rspec tests. More about the OpenAPI
140
+ initiative here: http://spec.openapis.org/'
141
+ email:
142
+ - gracefulpotatow@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".rubocop_rspec_alias_config.yml"
148
+ - MIT-LICENSE
149
+ - Rakefile
150
+ - lib/rspec/swag.rb
151
+ - lib/rspec/swag/configuration.rb
152
+ - lib/rspec/swag/example_group_helpers.rb
153
+ - lib/rspec/swag/example_helpers.rb
154
+ - lib/rspec/swag/extended_schema.rb
155
+ - lib/rspec/swag/project_initializer.rb
156
+ - lib/rspec/swag/rake_task.rb
157
+ - lib/rspec/swag/request_factory.rb
158
+ - lib/rspec/swag/response_validator.rb
159
+ - lib/rspec/swag/swagger_formatter.rb
160
+ - lib/rspec/swag/tasks/rspec_swag_tasks.rake
161
+ - lib/rspec/swag/templates/spec/swagger_helper.rb
162
+ - lib/rspec/swag/version.rb
163
+ homepage: https://github.com/graceful-potato/rspec-swag
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubygems_version: 3.4.20
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: An OpenAPI-based (formerly called Swagger) DSL for rspec & accompanying rake
186
+ task for generating OpenAPI specification files
187
+ test_files: []