open_api-rswag-specs 0.0.4

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: 16dfa3086e6aac66830a09692c5a2bf9309218f046aae1fc7ac7651821e23ce8
4
+ data.tar.gz: 0b05200a365269ebff5cbffe64fc61a2fc81c5adb604ab57b54410f072d0b09a
5
+ SHA512:
6
+ metadata.gz: eef7b341f14de4c3339ee4f7828b2c2729133de1779328403e84314f274ed286ac8cdbacebf0067fb72b4a7e29d4a35be4c7883f717c8c9687b17a52e2aa2537
7
+ data.tar.gz: b9ad36c2bd9980862b1c96f7b65ff0056b37808aca5cfe8db0cc794649131bc59f300bd778b75e1733877693dfe907f33560e283697c923c816fd151c495ab68
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 domaindrivendev
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,27 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'rswag-specs'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Adds swagger_helper to enable Swagger DSL in integration specs
3
+
4
+ Example:
5
+ rails generate rswag:specs:install
6
+
7
+ This will create:
8
+ spec/swagger_helper.rb
@@ -0,0 +1,14 @@
1
+ require 'rails/generators'
2
+
3
+ module Rswag
4
+ module Specs
5
+
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ def add_swagger_helper
10
+ template('swagger_helper.rb', 'spec/swagger_helper.rb')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,35 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.configure do |config|
4
+ # Specify a root folder where Swagger JSON files are generated
5
+ # NOTE: If you're using the rswag-api to serve API descriptions, you'll need
6
+ # to ensure that it's configured to serve Swagger from the same folder
7
+ config.swagger_root = Rails.root.join('swagger').to_s
8
+
9
+ # Define one or more Swagger documents and provide global metadata for each one
10
+ # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
11
+ # be generated at the provided relative path under swagger_root
12
+ # By default, the operations defined in spec files are added to the first
13
+ # document below. You can override this behavior by adding a swagger_doc tag to the
14
+ # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
15
+ config.swagger_docs = {
16
+ 'v1/swagger.json' => {
17
+ openapi: '3.0.0',
18
+ info: {
19
+ title: 'API V1',
20
+ version: 'v1'
21
+ },
22
+ paths: {},
23
+ servers: [
24
+ {
25
+ url: 'https://{defaultHost}',
26
+ variables: {
27
+ defaultHost: {
28
+ default: 'www.example.com'
29
+ }
30
+ }
31
+ }
32
+ ]
33
+ }
34
+ }
35
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenApi
4
+ module Rswag
5
+ module Specs
6
+ class Configuration
7
+ def initialize(rspec_config)
8
+ @rspec_config = rspec_config
9
+ end
10
+
11
+ def swagger_root
12
+ @swagger_root ||= begin
13
+ if @rspec_config.swagger_root.nil?
14
+ raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
15
+ end
16
+
17
+ @rspec_config.swagger_root
18
+ end
19
+ end
20
+
21
+ def swagger_docs
22
+ @swagger_docs ||= begin
23
+ if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
24
+ raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
25
+ end
26
+
27
+ @rspec_config.swagger_docs
28
+ end
29
+ end
30
+
31
+ def swagger_dry_run
32
+ @swagger_dry_run ||= begin
33
+ @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
34
+ end
35
+ end
36
+
37
+ def get_swagger_doc(name)
38
+ return swagger_docs.values.first if name.nil?
39
+ raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
40
+
41
+ swagger_docs[name]
42
+ end
43
+ end
44
+
45
+ class ConfigurationError < StandardError; end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+ require 'hashie'
3
+
4
+ module OpenApi
5
+ module Rswag
6
+ module Specs
7
+ module ExampleGroupHelpers
8
+ def path(template, metadata = {}, &block)
9
+ metadata[:path_item] = { template: template }
10
+ describe(template, metadata, &block)
11
+ end
12
+
13
+ %i[get post patch put delete head].each do |verb|
14
+ define_method(verb) do |summary, &block|
15
+ api_metadata = { operation: { verb: verb, summary: summary } }
16
+ describe(verb, api_metadata, &block)
17
+ end
18
+ end
19
+
20
+ %i[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
+ %i[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
+ # NICE TO HAVE
43
+ # TODO: update generator templates to include 3.0 syntax
44
+ # TODO: perform a tagged commit to trigger rubygem push for v 0.0.1
45
+
46
+ # MUST HAVES
47
+ # need to run ```npm install``` in rswag-ui dir to get assets to load
48
+ # not sure if its an asset issue or what but this should load => http://localhost:3000/api-docs/index.html
49
+ # TODO: fix examples in the main README
50
+
51
+ def request_body(attributes)
52
+ # can make this generic, and accept any incoming hash (like parameter method)
53
+ attributes.compact!
54
+
55
+ if metadata[:operation][:requestBody].blank?
56
+ metadata[:operation][:requestBody] = attributes
57
+ elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
58
+ # merge in
59
+ content_hash = metadata[:operation][:requestBody][:content]
60
+ incoming_content_hash = attributes[:content]
61
+ content_hash.merge!(incoming_content_hash) if incoming_content_hash
62
+ end
63
+ end
64
+
65
+ def request_body_json(schema:, required: true, description: nil, examples: nil)
66
+ passed_examples = Array(examples)
67
+ content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
68
+ request_body(description: description, required: required, content: content_hash)
69
+ if passed_examples.any?
70
+ # the request_factory is going to have to resolve the different ways that the example can be given
71
+ # it can contain a 'value' key which is a direct hash (easiest)
72
+ # it can contain a 'external_value' key which makes an external call to load the json
73
+ # it can contain a '$ref' key. Which points to #/components/examples/blog
74
+ passed_examples.each do |passed_example|
75
+ if passed_example.is_a?(Symbol)
76
+ example_key_name = passed_example
77
+ # TODO: write more tests around this adding to the parameter
78
+ # if symbol try and use save_request_example
79
+ param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
80
+ parameter(param_attributes)
81
+ elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
82
+ param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
83
+ parameter(param_attributes)
84
+ elsif passed_example.is_a?(Hash) && passed_example['$ref']
85
+ param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
86
+ parameter(param_attributes)
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def request_body_text_plain(required: false, description: nil, examples: nil)
93
+ content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
94
+ request_body(description: description, required: required, content: content_hash)
95
+ end
96
+
97
+ # TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
98
+ def request_body_xml(schema:,required: false, description: nil, examples: nil)
99
+ passed_examples = Array(examples)
100
+ content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
101
+ request_body(description: description, required: required, content: content_hash)
102
+ end
103
+
104
+ def request_body_multipart(schema:, description: nil)
105
+ content_hash = { 'multipart/form-data' => { schema: schema }}
106
+ request_body(description: description, content: content_hash)
107
+
108
+ schema.extend(Hashie::Extensions::DeepLocate)
109
+ file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary }
110
+
111
+ hash_locator = []
112
+
113
+ file_properties.each do |match|
114
+ hash_match = schema.deep_locate -> (_k, v, _obj) { v == match }
115
+ hash_locator.concat(hash_match) unless hash_match.empty?
116
+ end
117
+
118
+ property_hashes = hash_locator.flat_map do |locator|
119
+ locator.select { |_k,v| file_properties.include?(v) }
120
+ end
121
+
122
+ property_hashes.each do |property_hash|
123
+ file_name = property_hash.keys.first
124
+ parameter name: file_name, in: :formData, type: :file, required: true
125
+ end
126
+ end
127
+
128
+ def parameter(attributes)
129
+ if attributes[:in] && attributes[:in].to_sym == :path
130
+ attributes[:required] = true
131
+ end
132
+
133
+ if attributes[:type] && attributes[:schema].nil?
134
+ attributes[:schema] = {type: attributes[:type]}
135
+ end
136
+
137
+ if metadata.key?(:operation)
138
+ metadata[:operation][:parameters] ||= []
139
+ metadata[:operation][:parameters] << attributes
140
+ else
141
+ metadata[:path_item][:parameters] ||= []
142
+ metadata[:path_item][:parameters] << attributes
143
+ end
144
+ end
145
+
146
+ def response(code, description, metadata = {}, &block)
147
+ metadata[:response] = { code: code, description: description }
148
+ context(description, metadata, &block)
149
+ end
150
+
151
+ def schema(value, content_type: 'application/json')
152
+ content_hash = {content_type => {schema: value}}
153
+ metadata[:response][:content] = content_hash
154
+ end
155
+
156
+ def header(name, attributes)
157
+ metadata[:response][:headers] ||= {}
158
+
159
+ if attributes[:type] && attributes[:schema].nil?
160
+ attributes[:schema] = {type: attributes[:type]}
161
+ attributes.delete(:type)
162
+ end
163
+
164
+ metadata[:response][:headers][name] = attributes
165
+ end
166
+
167
+ # NOTE: Similar to 'description', 'examples' need to handle the case when
168
+ # being invoked with no params to avoid overriding 'examples' method of
169
+ # rspec-core ExampleGroup
170
+ def examples(example = nil)
171
+ return super() if example.nil?
172
+
173
+ metadata[:response][:examples] = example
174
+ end
175
+
176
+ # checks the examples in the parameters should be able to add $ref and externalValue examples.
177
+ # This syntax would look something like this in the integration _spec.rb file
178
+ #
179
+ # request_body_json schema: { '$ref' => '#/components/schemas/blog' },
180
+ # examples: [:blog, {name: :external_blog,
181
+ # externalValue: 'http://api.sample.org/myjson_example'},
182
+ # {name: :another_example,
183
+ # '$ref' => '#/components/examples/flexible_blog_example'}]
184
+ # The first value :blog, points to a let param of the same name, and is used to make the request in the
185
+ # integration test (it is used to build the request payload)
186
+ #
187
+ # The second item in the array shows how to add an externalValue for the examples in the requestBody section
188
+ # The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
189
+ #
190
+ # NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
191
+ # will not show it yet
192
+ def merge_other_examples!(example_metadata)
193
+ # example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
194
+ content_node = example_metadata[:operation][:requestBody][:content]['application/json']
195
+ return unless content_node
196
+
197
+ external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
198
+ ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
199
+ examples_node = content_node[:examples] ||= {}
200
+
201
+ nodes_to_add = []
202
+ nodes_to_add << external_example unless external_example.empty?
203
+ nodes_to_add << ref_example unless ref_example.empty?
204
+
205
+ nodes_to_add.each do |node|
206
+ json_request_examples = examples_node ||= {}
207
+ other_name = node[:name][:name]
208
+ other_key = node[:name][:externalValue] ? :externalValue : '$ref'
209
+ if other_name
210
+ json_request_examples.merge!(other_name => {other_key => node[:param_value]})
211
+ end
212
+ end
213
+ end
214
+
215
+ def run_test!(&block)
216
+ # NOTE: rspec 2.x support
217
+ if RSPEC_VERSION < 3
218
+ before do
219
+ submit_request(example.metadata)
220
+ end
221
+
222
+ it "returns a #{metadata[:response][:code]} response" do
223
+ assert_response_matches_metadata(metadata)
224
+ block.call(response) if block_given?
225
+ end
226
+ else
227
+ before do |example|
228
+ submit_request(example.metadata) #
229
+ end
230
+
231
+ it "returns a #{metadata[:response][:code]} response" do |example|
232
+ assert_response_matches_metadata(example.metadata, &block)
233
+ example.instance_exec(response, &block) if block_given?
234
+ end
235
+
236
+ after do |example|
237
+ body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] }
238
+
239
+ if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json']
240
+ # save response examples by default
241
+ example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty?
242
+
243
+ # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
244
+ if response.code.to_s =~ /^2\d{2}$/
245
+ example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
246
+ json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
247
+ json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
248
+
249
+ example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
250
+ end
251
+ end
252
+
253
+ self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
254
+
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open_api/rswag/specs/request_factory'
4
+ require 'open_api/rswag/specs/response_validator'
5
+
6
+ module OpenApi
7
+ module Rswag
8
+ module Specs
9
+ module ExampleHelpers
10
+ def submit_request(metadata)
11
+ request = RequestFactory.new.build_request(metadata, self)
12
+
13
+ if RAILS_VERSION < 5
14
+ send(
15
+ request[:verb],
16
+ request[:path],
17
+ request[:payload],
18
+ request[:headers]
19
+ )
20
+ else
21
+ send(
22
+ request[:verb],
23
+ request[:path],
24
+ params: request[:payload],
25
+ headers: request[:headers]
26
+ )
27
+ end
28
+ end
29
+
30
+ def assert_response_matches_metadata(metadata)
31
+ ResponseValidator.new.validate!(metadata, response)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json-schema'
4
+
5
+ module OpenApi
6
+ module Rswag
7
+ module Specs
8
+ class ExtendedSchema < JSON::Schema::Draft4
9
+ def initialize
10
+ super
11
+ @attributes['type'] = ExtendedTypeAttribute
12
+ @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
13
+ @names = ['http://tempuri.org/rswag/specs/extended_schema']
14
+ end
15
+ end
16
+
17
+ class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
18
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
19
+ return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
20
+
21
+ super
22
+ end
23
+ end
24
+
25
+ JSON::Validator.register_validator(ExtendedSchema.new)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenApi
4
+ module Rswag
5
+ module Specs
6
+ class Railtie < ::Rails::Railtie
7
+ rake_tasks do
8
+ load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __dir__)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'active_support/core_ext/hash/conversions'
5
+ require 'json'
6
+
7
+ module OpenApi
8
+ module Rswag
9
+ module Specs
10
+ class RequestFactory
11
+ def initialize(config = ::OpenApi::Rswag::Specs.config)
12
+ @config = config
13
+ end
14
+
15
+ def build_request(metadata, example)
16
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
17
+ parameters = expand_parameters(metadata, swagger_doc, example)
18
+
19
+ {}.tap do |request|
20
+ add_verb(request, metadata)
21
+ add_path(request, metadata, swagger_doc, parameters, example)
22
+ add_headers(request, metadata, swagger_doc, parameters, example)
23
+ add_payload(request, parameters, example)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def expand_parameters(metadata, swagger_doc, example)
30
+ operation_params = metadata[:operation][:parameters] || []
31
+ path_item_params = metadata[:path_item][:parameters] || []
32
+ security_params = derive_security_params(metadata, swagger_doc)
33
+
34
+ # NOTE: Use of + instead of concat to avoid mutation of the metadata object
35
+ (operation_params + path_item_params + security_params)
36
+ .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
37
+ .uniq { |p| p[:name] }
38
+ .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
39
+ end
40
+
41
+ def derive_security_params(metadata, swagger_doc)
42
+ requirements = metadata[:operation][:security] || swagger_doc[:security] || []
43
+ scheme_names = requirements.flat_map(&:keys)
44
+ components = swagger_doc[:components] || {}
45
+ schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values
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 resolve_parameter(ref, swagger_doc)
54
+ key = ref.sub('#/parameters/', '').to_sym
55
+ definitions = swagger_doc[:parameters]
56
+ raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
57
+
58
+ definitions[key]
59
+ end
60
+
61
+ def add_verb(request, metadata)
62
+ request[:verb] = metadata[:operation][:verb]
63
+ end
64
+
65
+ def add_path(request, metadata, swagger_doc, parameters, example)
66
+ template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
67
+
68
+ request[:path] = template.tap do |template|
69
+ parameters.select { |p| p[:in] == :path }.each do |p|
70
+ template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
71
+ end
72
+
73
+ parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
74
+ template.concat(i == 0 ? '?' : '&')
75
+ template.concat(build_query_string_part(p, example.send(p[:name])))
76
+ end
77
+ end
78
+ end
79
+
80
+ def build_query_string_part(param, value)
81
+ name = param[:name]
82
+ return "#{name}=#{value}" unless param[:type].to_sym == :array
83
+
84
+ case param[:collectionFormat]
85
+ when :ssv
86
+ "#{name}=#{value.join(' ')}"
87
+ when :tsv
88
+ "#{name}=#{value.join('\t')}"
89
+ when :pipes
90
+ "#{name}=#{value.join('|')}"
91
+ when :multi
92
+ value.map { |v| "#{name}=#{v}" }.join('&')
93
+ else
94
+ "#{name}=#{value.join(',')}" # csv is default
95
+ end
96
+ end
97
+
98
+ def add_headers(request, metadata, swagger_doc, parameters, example)
99
+ tuples = parameters
100
+ .select { |p| p[:in] == :header }
101
+ .map { |p| [p[:name], example.send(p[:name]).to_s] }
102
+
103
+ # Accept header
104
+ produces = metadata[:operation][:produces] || swagger_doc[:produces]
105
+ if produces
106
+ accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
107
+ tuples << ['Accept', accept]
108
+ end
109
+
110
+ # Content-Type header
111
+ consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
112
+ if consumes
113
+ content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
114
+ tuples << ['Content-Type', content_type]
115
+ end
116
+
117
+ # Rails test infrastructure requires rackified headers
118
+ rackified_tuples = tuples.map do |pair|
119
+ [
120
+ case pair[0]
121
+ when 'Accept' then 'HTTP_ACCEPT'
122
+ when 'Content-Type' then 'CONTENT_TYPE'
123
+ when 'Authorization' then 'HTTP_AUTHORIZATION'
124
+ else pair[0]
125
+ end,
126
+ pair[1]
127
+ ]
128
+ end
129
+
130
+ request[:headers] = Hash[rackified_tuples]
131
+ end
132
+
133
+ def add_payload(request, parameters, example)
134
+ content_type = request[:headers]['CONTENT_TYPE']
135
+ return if content_type.nil?
136
+
137
+ if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
138
+ request[:payload] = build_form_payload(parameters, example)
139
+ else
140
+ request[:payload] = build_json_payload(parameters, example)
141
+ end
142
+ end
143
+
144
+ def build_form_payload(parameters, example)
145
+ # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
146
+ # Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
147
+ # Rails test infrastructure allows us to send the values directly as a hash
148
+ # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
149
+ tuples = parameters
150
+ .select { |p| p[:in] == :formData }
151
+ .map { |p| [p[:name], example.send(p[:name])] }
152
+ Hash[tuples]
153
+ end
154
+
155
+ def build_json_payload(parameters, example)
156
+ body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
157
+ return nil unless body_param
158
+
159
+ source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
160
+ source_body_param ||= body_param[:param_value]
161
+ source_body_param ? source_body_param.to_json : nil
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'json-schema'
5
+ require 'json'
6
+ require 'open_api/rswag/specs/extended_schema'
7
+
8
+ module OpenApi
9
+ module Rswag
10
+ module Specs
11
+ class ResponseValidator
12
+ def initialize(config = ::OpenApi::Rswag::Specs.config)
13
+ @config = config
14
+ end
15
+
16
+ def validate!(metadata, response)
17
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
18
+
19
+ validate_code!(metadata, response)
20
+ validate_headers!(metadata, response.headers)
21
+ validate_body!(metadata, swagger_doc, response.body)
22
+ end
23
+
24
+ private
25
+
26
+ def validate_code!(metadata, response)
27
+ expected = metadata[:response][:code].to_s
28
+ if response.code != expected
29
+ raise UnexpectedResponse,
30
+ "Expected response code '#{response.code}' to match '#{expected}'\n" \
31
+ "Response body: #{response.body}"
32
+ end
33
+ end
34
+
35
+ def validate_headers!(metadata, headers)
36
+ expected = (metadata[:response][:headers] || {}).keys
37
+ expected.each do |name|
38
+ raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
39
+ end
40
+ end
41
+
42
+ def validate_body!(metadata, swagger_doc, body)
43
+ test_schemas = extract_schemas(metadata)
44
+ return if test_schemas.nil? || test_schemas.empty?
45
+
46
+ components = swagger_doc[:components] || {}
47
+ components_schemas = { components: { schemas: components[:schemas] } }
48
+
49
+ validation_schema = test_schemas[:schema] # response_schema
50
+ .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
51
+ .merge(components_schemas)
52
+
53
+ errors = JSON::Validator.fully_validate(validation_schema, body)
54
+ raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
55
+ end
56
+
57
+ def extract_schemas(metadata)
58
+ metadata[:operation] = {produces: []} if metadata[:operation].nil?
59
+ produces = Array(metadata[:operation][:produces])
60
+
61
+ producer_content = produces.first || 'application/json'
62
+ response_content = metadata[:response][:content] || {producer_content => {}}
63
+ response_content[producer_content]
64
+ end
65
+ end
66
+
67
+ class UnexpectedResponse < StandardError; end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/deep_merge'
4
+ require 'swagger_helper'
5
+
6
+ module OpenApi
7
+ module Rswag
8
+ module Specs
9
+ class SwaggerFormatter
10
+ # NOTE: rspec 2.x support
11
+ if RSPEC_VERSION > 2
12
+ ::RSpec::Core::Formatters.register self, :example_group_finished, :stop
13
+ end
14
+
15
+ def initialize(output, config = ::OpenApi::Rswag::Specs.config)
16
+ @output = output
17
+ @config = config
18
+
19
+ @output.puts 'Generating Swagger docs ...'
20
+ end
21
+
22
+ def example_group_finished(notification)
23
+ # NOTE: rspec 2.x support
24
+ metadata = if RSPEC_VERSION > 2
25
+ notification.group.metadata
26
+ else
27
+ notification.metadata
28
+ end
29
+
30
+ return unless metadata.key?(:response)
31
+
32
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
33
+ swagger_doc.deep_merge!(metadata_to_swagger(metadata))
34
+ end
35
+
36
+ def stop(_notification = nil)
37
+ @config.swagger_docs.each do |url_path, doc|
38
+ # remove 2.0 parameters
39
+ doc[:paths]&.each_pair do |_k, v|
40
+ v.each_pair do |_verb, value|
41
+ is_hash = value.is_a?(Hash)
42
+ if is_hash && value.dig(:parameters)
43
+ schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] }
44
+ if value && schema_param && value&.dig(:requestBody, :content, 'application/json')
45
+ value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema])
46
+ end
47
+
48
+ value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
49
+ value[:parameters].each { |p| p.delete(:type) }
50
+ value[:headers].each { |p| p.delete(:type)} if value[:headers]
51
+ end
52
+
53
+ value.delete(:consumes) if is_hash && value.dig(:consumes)
54
+ value.delete(:produces) if is_hash && value.dig(:produces)
55
+ end
56
+ end
57
+
58
+ file_path = File.join(@config.swagger_root, url_path)
59
+ dirname = File.dirname(file_path)
60
+ FileUtils.mkdir_p dirname unless File.exist?(dirname)
61
+
62
+ File.open(file_path, 'w') do |file|
63
+ file.write(JSON.pretty_generate(doc))
64
+ end
65
+
66
+ @output.puts "Swagger doc generated at #{file_path}"
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def metadata_to_swagger(metadata)
73
+ response_code = metadata[:response][:code]
74
+ response = metadata[:response].reject { |k, _v| k == :code }
75
+
76
+ # need to merge in to response
77
+ if response[:examples]&.dig('application/json')
78
+ example = response[:examples].dig('application/json').dup
79
+ schema = response.dig(:content, 'application/json', :schema)
80
+ new_hash = {example: example}
81
+ new_hash[:schema] = schema if schema
82
+ response.merge!(content: { 'application/json' => new_hash })
83
+ response.delete(:examples)
84
+ end
85
+
86
+
87
+ verb = metadata[:operation][:verb]
88
+ operation = metadata[:operation]
89
+ .reject { |k, _v| k == :verb }
90
+ .merge(responses: { response_code => response })
91
+
92
+ path_template = metadata[:path_item][:template]
93
+ path_item = metadata[:path_item]
94
+ .reject { |k, _v| k == :template }
95
+ .merge(verb => operation)
96
+
97
+ { paths: { path_template => path_item } }
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,29 @@
1
+ require 'rspec/core'
2
+ require 'open_api/rswag/specs/example_group_helpers'
3
+ require 'open_api/rswag/specs/example_helpers'
4
+ require 'open_api/rswag/specs/configuration'
5
+ require 'open_api/rswag/specs/railtie' if defined?(Rails::Railtie)
6
+
7
+ module OpenApi
8
+ module Rswag
9
+ module Specs
10
+
11
+ # Extend RSpec with a swagger-based DSL
12
+ ::RSpec.configure do |c|
13
+ c.add_setting :swagger_root
14
+ c.add_setting :swagger_docs
15
+ c.add_setting :swagger_dry_run
16
+ c.extend ExampleGroupHelpers, type: :request
17
+ c.include ExampleHelpers, type: :request
18
+ end
19
+
20
+ def self.config
21
+ @config ||= Configuration.new(RSpec.configuration)
22
+ end
23
+
24
+ # Support Rails 3+ and RSpec 2+ (sigh!)
25
+ RAILS_VERSION = Rails::VERSION::MAJOR
26
+ RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ namespace :rswag do
4
+ namespace :specs do
5
+
6
+ desc 'Generate Swagger JSON files from integration specs'
7
+ RSpec::Core::RakeTask.new('swaggerize') do |t|
8
+ t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
9
+
10
+ # NOTE: rspec 2.x support
11
+ if OpenApi::Rswag::Specs::RSPEC_VERSION > 2 && OpenApi::Rswag::Specs.config.swagger_dry_run
12
+ t.rspec_opts = [ '--format OpenApi::Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
13
+ else
14
+ t.rspec_opts = [ '--format OpenApi::Rswag::Specs::SwaggerFormatter', '--order defined' ]
15
+ end
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: open_api-rswag-specs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Richie Morris
8
+ - Jay Danielian
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-08-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '3.1'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '6.0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '3.1'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ - !ruby/object:Gem::Dependency
35
+ name: json-schema
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ - !ruby/object:Gem::Dependency
49
+ name: railties
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - - "<"
56
+ - !ruby/object:Gem::Version
57
+ version: '6.0'
58
+ type: :runtime
59
+ prerelease: false
60
+ version_requirements: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '3.1'
65
+ - - "<"
66
+ - !ruby/object:Gem::Version
67
+ version: '6.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: hashie
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: guard-rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ description: Simplify API integration testing with a succinct rspec DSL and generate
97
+ Swagger files directly from your rspecs
98
+ email:
99
+ - domaindrivendev@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - MIT-LICENSE
105
+ - Rakefile
106
+ - lib/generators/rswag/specs/install/USAGE
107
+ - lib/generators/rswag/specs/install/install_generator.rb
108
+ - lib/generators/rswag/specs/install/templates/swagger_helper.rb
109
+ - lib/open_api/rswag/specs.rb
110
+ - lib/open_api/rswag/specs/configuration.rb
111
+ - lib/open_api/rswag/specs/example_group_helpers.rb
112
+ - lib/open_api/rswag/specs/example_helpers.rb
113
+ - lib/open_api/rswag/specs/extended_schema.rb
114
+ - lib/open_api/rswag/specs/railtie.rb
115
+ - lib/open_api/rswag/specs/request_factory.rb
116
+ - lib/open_api/rswag/specs/response_validator.rb
117
+ - lib/open_api/rswag/specs/swagger_formatter.rb
118
+ - lib/tasks/rswag-specs_tasks.rake
119
+ homepage: https://github.com/jdanielian/open-api-rswag
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.0.4
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: A Swagger-based DSL for rspec-rails & accompanying rake task for generating
142
+ Swagger files
143
+ test_files: []