rspec-swag 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: []