openapi-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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7fe4364f6d0dabffc857791ff28c23dc133635743b276d7131099e2b981ebcc2
4
+ data.tar.gz: c2a7f905dfed6852891d1b5a6bdaa4720efb7a3bada3c5b5b735f0924374ea5e
5
+ SHA512:
6
+ metadata.gz: 925d424f35bf7d0d8d58766e6b2b7752be4f0a1b414dbc2757fe7af12e70bcdc624ad61e009a6b82427a570835ab721ec94d454c0481dce87f36510aa9d9a95e
7
+ data.tar.gz: 4ae03e3e2f9a7e851fa0e3d8115dcc8d536b7f7648ce409bb57ed3d3e4a96917a80500a507c327387d5a59612c270783dc73c4ff059d3fe717e6b3b499ebc984
@@ -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.
@@ -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,9 @@
1
+ Description:
2
+ This creates an RSpec request spec to define Swagger documentation for a
3
+ controller. It will create a test for each of the controller's methods.
4
+
5
+ Example:
6
+ rails generate rspec:swagger V3::AccountsController
7
+
8
+ This will create:
9
+ spec/requests/v3/accounts_spec.rb
@@ -0,0 +1,22 @@
1
+ require 'openapi/rswag/route_parser'
2
+ require 'rails/generators'
3
+
4
+ module Rspec
5
+ class SwaggerGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def setup
9
+ @routes = Openapi::Rswag::RouteParser.new(controller_path).routes
10
+ end
11
+
12
+ def create_spec_file
13
+ template 'spec.rb', File.join('spec', 'requests', "#{controller_path}_spec.rb")
14
+ end
15
+
16
+ private
17
+
18
+ def controller_path
19
+ file_path.chomp('_controller')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ require 'swagger_helper'
2
+
3
+ RSpec.describe '<%= controller_path %>', type: :request do
4
+ <% @routes.each do | template, path_item | %>
5
+ path '<%= template %>' do
6
+ <% unless path_item[:params].empty? -%>
7
+ # You'll want to customize the parameter types...
8
+ <% path_item[:params].each do |param| -%>
9
+ parameter name: '<%= param %>', in: :path, type: :string, description: '<%= param %>'
10
+ <% end -%>
11
+ <% end -%>
12
+ <% path_item[:actions].each do | action, details | %>
13
+ <%= action %>('<%= details[:summary] %>') do
14
+ response(200, 'successful') do
15
+ <% unless path_item[:params].empty? -%>
16
+ <% path_item[:params].each do |param| -%>
17
+ let(:<%= param %>) { '123' }
18
+ <% end -%>
19
+ <% end -%>
20
+
21
+ after do |example|
22
+ example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
23
+ end
24
+ run_test!
25
+ end
26
+ end
27
+ <% end -%>
28
+ end
29
+ <% end -%>
30
+ end
@@ -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,31 @@
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.yaml' => {
17
+ openapi: '3.0.1',
18
+ info: {
19
+ title: 'API V1',
20
+ version: 'v1'
21
+ },
22
+ paths: {}
23
+ }
24
+ }
25
+
26
+ # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
27
+ # The swagger_docs configuration option has the filename including format in
28
+ # the key, this may want to be changed to avoid putting yaml in json files.
29
+ # Defaults to json. Accepts ':json' and ':yaml'.
30
+ config.swagger_format = :yaml
31
+ end
@@ -0,0 +1,60 @@
1
+ module Openapi
2
+ module Rswag
3
+ class RouteParser
4
+ attr_reader :controller
5
+
6
+ def initialize(controller)
7
+ @controller = controller
8
+ end
9
+
10
+ def routes
11
+ ::Rails.application.routes.routes.select do |route|
12
+ route.defaults[:controller] == controller
13
+ end.reduce({}) do |tree, route|
14
+ path = path_from(route)
15
+ verb = verb_from(route)
16
+ tree[path] ||= { params: params_from(route), actions: {} }
17
+ tree[path][:actions][verb] = { summary: summary_from(route) }
18
+ tree
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def path_from(route)
25
+ route.path.spec.to_s
26
+ .chomp('(.:format)') # Ignore any format suffix
27
+ .gsub(/:([^\/.?]+)/, '{\1}') # Convert :id to {id}
28
+ end
29
+
30
+ def verb_from(route)
31
+ verb = route.verb
32
+ if verb.kind_of? String
33
+ verb.downcase
34
+ else
35
+ verb.source.gsub(/[$^]/, '').downcase
36
+ end
37
+ end
38
+
39
+ def summary_from(route)
40
+ verb = route.requirements[:action]
41
+ noun = route.requirements[:controller].split('/').last.singularize
42
+
43
+ # Apply a few customizations to make things more readable
44
+ case verb
45
+ when 'index'
46
+ verb = 'list'
47
+ noun = noun.pluralize
48
+ when 'destroy'
49
+ verb = 'delete'
50
+ end
51
+
52
+ "#{verb} #{noun}"
53
+ end
54
+
55
+ def params_from(route)
56
+ route.segments - ['format']
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ require 'rspec/core'
2
+ require 'openapi/rswag/specs/example_group_helpers'
3
+ require 'openapi/rswag/specs/example_helpers'
4
+ require 'openapi/rswag/specs/configuration'
5
+ require 'openapi/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.add_setting :swagger_format
17
+ c.extend ExampleGroupHelpers, type: :request
18
+ c.include ExampleHelpers, type: :request
19
+ end
20
+
21
+ def self.config
22
+ @config ||= Configuration.new(RSpec.configuration)
23
+ end
24
+
25
+ # Support Rails 3+ and RSpec 2+ (sigh!)
26
+ RAILS_VERSION = Rails::VERSION::MAJOR
27
+ RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ module Openapi
2
+ module Rswag
3
+ module Specs
4
+
5
+ class Configuration
6
+
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
+ @rspec_config.swagger_root
17
+ end
18
+ end
19
+
20
+ def swagger_docs
21
+ @swagger_docs ||= begin
22
+ if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
23
+ raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
24
+ end
25
+ @rspec_config.swagger_docs
26
+ end
27
+ end
28
+
29
+ def swagger_dry_run
30
+ @swagger_dry_run ||= begin
31
+ @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
32
+ end
33
+ end
34
+
35
+ def swagger_format
36
+ @swagger_format ||= begin
37
+ @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty?
38
+ raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
39
+ @rspec_config.swagger_format
40
+ end
41
+ end
42
+
43
+ def get_swagger_doc(name)
44
+ return swagger_docs.values.first if name.nil?
45
+ raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
46
+ swagger_docs[name]
47
+ end
48
+ end
49
+
50
+ class ConfigurationError < StandardError; end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,100 @@
1
+ module Openapi
2
+ module Rswag
3
+ module Specs
4
+ module ExampleGroupHelpers
5
+
6
+ def path(template, metadata={}, &block)
7
+ metadata[:path_item] = { template: template }
8
+ describe(template, metadata, &block)
9
+ end
10
+
11
+ [ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb|
12
+ define_method(verb) do |summary, &block|
13
+ api_metadata = { operation: { verb: verb, summary: summary } }
14
+ describe(verb, api_metadata, &block)
15
+ end
16
+ end
17
+
18
+ [ :operationId, :deprecated, :security ].each do |attr_name|
19
+ define_method(attr_name) do |value|
20
+ metadata[:operation][attr_name] = value
21
+ end
22
+ end
23
+
24
+ # NOTE: 'description' requires special treatment because ExampleGroup already
25
+ # defines a method with that name. Provide an override that supports the existing
26
+ # functionality while also setting the appropriate metadata if applicable
27
+ def description(value=nil)
28
+ return super() if value.nil?
29
+ metadata[:operation][:description] = value
30
+ end
31
+
32
+ # These are array properties - note the splat operator
33
+ [ :tags, :consumes, :produces, :schemes ].each do |attr_name|
34
+ define_method(attr_name) do |*value|
35
+ metadata[:operation][attr_name] = value
36
+ end
37
+ end
38
+
39
+ def parameter(attributes)
40
+ if attributes[:in] && attributes[:in].to_sym == :path
41
+ attributes[:required] = true
42
+ end
43
+
44
+ if metadata.has_key?(:operation)
45
+ metadata[:operation][:parameters] ||= []
46
+ metadata[:operation][:parameters] << attributes
47
+ else
48
+ metadata[:path_item][:parameters] ||= []
49
+ metadata[:path_item][:parameters] << attributes
50
+ end
51
+ end
52
+
53
+ def response(code, description, metadata={}, &block)
54
+ metadata[:response] = { code: code, description: description }
55
+ context(description, metadata, &block)
56
+ end
57
+
58
+ def schema(value)
59
+ metadata[:response][:schema] = value
60
+ end
61
+
62
+ def header(name, attributes)
63
+ metadata[:response][:headers] ||= {}
64
+ metadata[:response][:headers][name] = attributes
65
+ end
66
+
67
+ # NOTE: Similar to 'description', 'examples' need to handle the case when
68
+ # being invoked with no params to avoid overriding 'examples' method of
69
+ # rspec-core ExampleGroup
70
+ def examples(example = nil)
71
+ return super() if example.nil?
72
+ metadata[:response][:examples] = example
73
+ end
74
+
75
+ def run_test!(&block)
76
+ # NOTE: rspec 2.x support
77
+ if RSPEC_VERSION < 3
78
+ before do
79
+ submit_request(example.metadata)
80
+ end
81
+
82
+ it "returns a #{metadata[:response][:code]} response" do
83
+ assert_response_matches_metadata(metadata)
84
+ block.call(response) if block_given?
85
+ end
86
+ else
87
+ before do |example|
88
+ submit_request(example.metadata)
89
+ end
90
+
91
+ it "returns a #{metadata[:response][:code]} response" do |example|
92
+ assert_response_matches_metadata(example.metadata, &block)
93
+ example.instance_exec(response, &block) if block_given?
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,37 @@
1
+ require 'openapi/rswag/specs/request_factory'
2
+ require 'openapi/rswag/specs/response_validator'
3
+
4
+ module Openapi
5
+ module Rswag
6
+ module Specs
7
+ module ExampleHelpers
8
+
9
+ def submit_request(metadata)
10
+ request = RequestFactory.new.build_request(metadata, self)
11
+
12
+ if RAILS_VERSION < 5
13
+ send(
14
+ request[:verb],
15
+ request[:path],
16
+ request[:payload],
17
+ request[:headers]
18
+ )
19
+ else
20
+ send(
21
+ request[:verb],
22
+ request[:path],
23
+ {
24
+ params: request[:payload],
25
+ headers: request[:headers]
26
+ }
27
+ )
28
+ end
29
+ end
30
+
31
+ def assert_response_matches_metadata(metadata)
32
+ ResponseValidator.new.validate!(metadata, response)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ require 'json-schema'
2
+
3
+ module Openapi
4
+ module Rswag
5
+ module Specs
6
+ class ExtendedSchema < JSON::Schema::Draft4
7
+
8
+ def initialize
9
+ super
10
+ @attributes['type'] = ExtendedTypeAttribute
11
+ @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
12
+ @names = ['http://tempuri.org/rswag/specs/extended_schema']
13
+ end
14
+ end
15
+
16
+ class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
17
+
18
+ def self.validate(current_schema, data, fragments, processor, validator, options={})
19
+ return if data.nil? && current_schema.schema['x-nullable'] == true
20
+ super
21
+ end
22
+ end
23
+
24
+ JSON::Validator.register_validator(ExtendedSchema.new)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Openapi
2
+ module Rswag
3
+ module Specs
4
+ class Railtie < ::Rails::Railtie
5
+
6
+ rake_tasks do
7
+ load File.expand_path('../../../../tasks/rswag-specs_tasks.rake', __FILE__)
8
+ end
9
+
10
+ generators do
11
+ require 'generators/rspec/swagger_generator.rb'
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,159 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'active_support/core_ext/hash/conversions'
3
+ require 'json'
4
+
5
+ module Openapi
6
+ module Rswag
7
+ module Specs
8
+ class RequestFactory
9
+
10
+ def initialize(config = ::Openapi::Rswag::Specs.config)
11
+ @config = config
12
+ end
13
+
14
+ def build_request(metadata, example)
15
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
16
+ parameters = expand_parameters(metadata, swagger_doc, example)
17
+
18
+ {}.tap do |request|
19
+ add_verb(request, metadata)
20
+ add_path(request, metadata, swagger_doc, parameters, example)
21
+ add_headers(request, metadata, swagger_doc, parameters, example)
22
+ add_payload(request, parameters, example)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def expand_parameters(metadata, swagger_doc, example)
29
+ operation_params = metadata[:operation][:parameters] || []
30
+ path_item_params = metadata[:path_item][:parameters] || []
31
+ security_params = derive_security_params(metadata, swagger_doc)
32
+
33
+ # NOTE: Use of + instead of concat to avoid mutation of the metadata object
34
+ (operation_params + path_item_params + security_params)
35
+ .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
36
+ .uniq { |p| p[:name] }
37
+ .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
38
+ end
39
+
40
+ def derive_security_params(metadata, swagger_doc)
41
+ requirements = metadata[:operation][:security] || swagger_doc[:security] || []
42
+ scheme_names = requirements.flat_map { |r| r.keys }
43
+ schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
44
+
45
+ schemes.map do |scheme|
46
+ param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
47
+ param.merge(type: :string, required: requirements.one?)
48
+ end
49
+ end
50
+
51
+ def resolve_parameter(ref, swagger_doc)
52
+ key = ref.sub('#/parameters/', '').to_sym
53
+ definitions = swagger_doc[:parameters]
54
+ raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
55
+ definitions[key]
56
+ end
57
+
58
+ def add_verb(request, metadata)
59
+ request[:verb] = metadata[:operation][:verb]
60
+ end
61
+
62
+ def add_path(request, metadata, swagger_doc, parameters, example)
63
+ template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
64
+
65
+ request[:path] = template.tap do |template|
66
+ parameters.select { |p| p[:in] == :path }.each do |p|
67
+ template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
68
+ end
69
+
70
+ parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
71
+ template.concat(i == 0 ? '?' : '&')
72
+ template.concat(build_query_string_part(p, example.send(p[:name])))
73
+ end
74
+ end
75
+ end
76
+
77
+ def build_query_string_part(param, value)
78
+ name = param[:name]
79
+ return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
80
+
81
+ case param[:collectionFormat]
82
+ when :ssv
83
+ "#{name}=#{value.join(' ')}"
84
+ when :tsv
85
+ "#{name}=#{value.join('\t')}"
86
+ when :pipes
87
+ "#{name}=#{value.join('|')}"
88
+ when :multi
89
+ value.map { |v| "#{name}=#{v}" }.join('&')
90
+ else
91
+ "#{name}=#{value.join(',')}" # csv is default
92
+ end
93
+ end
94
+
95
+ def add_headers(request, metadata, swagger_doc, parameters, example)
96
+ tuples = parameters
97
+ .select { |p| p[:in] == :header }
98
+ .map { |p| [ p[:name], example.send(p[:name]).to_s ] }
99
+
100
+ # Accept header
101
+ produces = metadata[:operation][:produces] || swagger_doc[:produces]
102
+ if produces
103
+ accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first
104
+ tuples << [ 'Accept', accept ]
105
+ end
106
+
107
+ # Content-Type header
108
+ consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
109
+ if consumes
110
+ content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
111
+ tuples << [ 'Content-Type', content_type ]
112
+ end
113
+
114
+ # Rails test infrastructure requires rackified headers
115
+ rackified_tuples = tuples.map do |pair|
116
+ [
117
+ case pair[0]
118
+ when 'Accept' then 'HTTP_ACCEPT'
119
+ when 'Content-Type' then 'CONTENT_TYPE'
120
+ when 'Authorization' then 'HTTP_AUTHORIZATION'
121
+ else pair[0]
122
+ end,
123
+ pair[1]
124
+ ]
125
+ end
126
+
127
+ request[:headers] = Hash[ rackified_tuples ]
128
+ end
129
+
130
+ def add_payload(request, parameters, example)
131
+ content_type = request[:headers]['CONTENT_TYPE']
132
+ return if content_type.nil?
133
+
134
+ if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type)
135
+ request[:payload] = build_form_payload(parameters, example)
136
+ else
137
+ request[:payload] = build_json_payload(parameters, example)
138
+ end
139
+ end
140
+
141
+ def build_form_payload(parameters, example)
142
+ # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
143
+ # Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
144
+ # Rails test infrastructure allows us to send the values directly as a hash
145
+ # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
146
+ tuples = parameters
147
+ .select { |p| p[:in] == :formData }
148
+ .map { |p| [ p[:name], example.send(p[:name]) ] }
149
+ Hash[ tuples ]
150
+ end
151
+
152
+ def build_json_payload(parameters, example)
153
+ body_param = parameters.select { |p| p[:in] == :body }.first
154
+ body_param ? example.send(body_param[:name]).to_json : nil
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'json-schema'
3
+ require 'json'
4
+ require 'openapi/rswag/specs/extended_schema'
5
+
6
+ module Openapi
7
+ module Rswag
8
+ module Specs
9
+ class ResponseValidator
10
+
11
+ def initialize(config = ::Openapi::Rswag::Specs.config)
12
+ @config = config
13
+ end
14
+
15
+ def validate!(metadata, response)
16
+ swagger_doc = @config.get_swagger_doc(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
+ if response.code != expected
28
+ raise UnexpectedResponse,
29
+ "Expected response code '#{response.code}' to match '#{expected}'\n" \
30
+ "Response body: #{response.body}"
31
+ end
32
+ end
33
+
34
+ def validate_headers!(metadata, headers)
35
+ expected = (metadata[:response][:headers] || {}).keys
36
+ expected.each do |name|
37
+ raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
38
+ end
39
+ end
40
+
41
+ def validate_body!(metadata, swagger_doc, body)
42
+ response_schema = metadata[:response][:schema]
43
+ return if response_schema.nil?
44
+
45
+ validation_schema = response_schema
46
+ .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
47
+ .merge(swagger_doc.slice(:definitions))
48
+ errors = JSON::Validator.fully_validate(validation_schema, body)
49
+ raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
50
+ end
51
+ end
52
+
53
+ class UnexpectedResponse < StandardError; end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_support/core_ext/hash/deep_merge'
2
+ require 'swagger_helper'
3
+
4
+ module Openapi
5
+ module Rswag
6
+ module Specs
7
+ class SwaggerFormatter
8
+
9
+ # NOTE: rspec 2.x support
10
+ if RSPEC_VERSION > 2
11
+ ::RSpec::Core::Formatters.register self, :example_group_finished, :stop
12
+ end
13
+
14
+ def initialize(output, config = Rswag::Specs.config)
15
+ @output = output
16
+ @config = config
17
+
18
+ @output.puts 'Generating Swagger docs ...'
19
+ end
20
+
21
+ def example_group_finished(notification)
22
+ # NOTE: rspec 2.x support
23
+ if RSPEC_VERSION > 2
24
+ metadata = notification.group.metadata
25
+ else
26
+ metadata = notification.metadata
27
+ end
28
+
29
+ # !metadata[:document] won't work, since nil means we should generate
30
+ # docs.
31
+ return if metadata[:document] == false
32
+ return unless metadata.has_key?(:response)
33
+
34
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
35
+ swagger_doc.deep_merge!(metadata_to_swagger(metadata))
36
+ end
37
+
38
+ def stop(notification=nil)
39
+ @config.swagger_docs.each do |url_path, doc|
40
+ file_path = File.join(@config.swagger_root, url_path)
41
+ dirname = File.dirname(file_path)
42
+ FileUtils.mkdir_p dirname unless File.exists?(dirname)
43
+
44
+ File.open(file_path, 'w') do |file|
45
+ file.write(pretty_generate(doc))
46
+ end
47
+
48
+ @output.puts "Swagger doc generated at #{file_path}"
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def pretty_generate(doc)
55
+ if @config.swagger_format == :yaml
56
+ clean_doc = yaml_prepare(doc)
57
+ YAML.dump(clean_doc)
58
+ else # config errors are thrown in 'def swagger_format', no throw needed here
59
+ JSON.pretty_generate(doc)
60
+ end
61
+ end
62
+
63
+ def yaml_prepare(doc)
64
+ json_doc = JSON.pretty_generate(doc)
65
+ clean_doc = JSON.parse(json_doc)
66
+ end
67
+
68
+ def metadata_to_swagger(metadata)
69
+ response_code = metadata[:response][:code]
70
+ response = metadata[:response].reject { |k,v| k == :code }
71
+
72
+ verb = metadata[:operation][:verb]
73
+ operation = metadata[:operation]
74
+ .reject { |k,v| k == :verb }
75
+ .merge(responses: { response_code => response })
76
+
77
+ path_template = metadata[:path_item][:template]
78
+ path_item = metadata[:path_item]
79
+ .reject { |k,v| k == :template }
80
+ .merge(verb => operation)
81
+
82
+ { paths: { path_template => path_item } }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,23 @@
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 = ENV.fetch(
9
+ 'PATTERN',
10
+ 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
11
+ )
12
+
13
+ # NOTE: rspec 2.x support
14
+ if Openapi::Rswag::Specs::RSPEC_VERSION > 2 && Openapi::Rswag::Specs.config.swagger_dry_run
15
+ t.rspec_opts = [ '--format Openapi::Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
16
+ else
17
+ t.rspec_opts = [ '--format Openapi::Rswag::Specs::SwaggerFormatter', '--order defined' ]
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ task :rswag => ['rswag:specs:swaggerize']
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openapi-rswag-specs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Emmanuel Ndangurura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: railties
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '7.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '7.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: json-schema
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.2'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.2'
67
+ description: Simplify API integration testing with a succinct rspec DSL and generate
68
+ Swagger files directly from your rspecs
69
+ email:
70
+ - endangurura@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - MIT-LICENSE
76
+ - Rakefile
77
+ - lib/generators/rspec/USAGE
78
+ - lib/generators/rspec/swagger_generator.rb
79
+ - lib/generators/rspec/templates/spec.rb
80
+ - lib/generators/rswag/specs/install/USAGE
81
+ - lib/generators/rswag/specs/install/install_generator.rb
82
+ - lib/generators/rswag/specs/install/templates/swagger_helper.rb
83
+ - lib/openapi/rswag/route_parser.rb
84
+ - lib/openapi/rswag/specs.rb
85
+ - lib/openapi/rswag/specs/configuration.rb
86
+ - lib/openapi/rswag/specs/example_group_helpers.rb
87
+ - lib/openapi/rswag/specs/example_helpers.rb
88
+ - lib/openapi/rswag/specs/extended_schema.rb
89
+ - lib/openapi/rswag/specs/railtie.rb
90
+ - lib/openapi/rswag/specs/request_factory.rb
91
+ - lib/openapi/rswag/specs/response_validator.rb
92
+ - lib/openapi/rswag/specs/swagger_formatter.rb
93
+ - lib/tasks/rswag-specs_tasks.rake
94
+ homepage: https://github.com/endangurura/rswag
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.1.2
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: A Swagger-based DSL for rspec-rails & accompanying rake task for generating
117
+ Swagger files
118
+ test_files: []