open_api-rswag-specs 0.0.4

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: 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: []