rswag-specs 2.1.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d360f480ad502e00f5951e40882af98b2b1cc9f4d414c65e41495ca77494a3b0
4
- data.tar.gz: 7492e978e971b80560cc1eba26b0b7426c6145745e6dea93be46a58ff200965f
3
+ metadata.gz: 540d37d1a9a9e90a4b062f65239029fdd9fcdcfec4622eb68ec4f90ec4eebab9
4
+ data.tar.gz: 9180d18a4a0d8ba624c42a38376270bdb0885da77f007cf001be3128d5929d66
5
5
  SHA512:
6
- metadata.gz: 352cd0dd07a2c39626f58388ffa1b069f30c538d666bab8206f7c027df7412a3fcaac79a9855e7c260b0c4a98ea0d5c58e12503cd665701a9ccfdeac6c9bf19c
7
- data.tar.gz: bf66adcb9ff47d01fe283782f87bb4a6ec996e72eddd1850cb498f81b665aecf49393bb480fad81b1ee71f68cd03dc90307658b9b1b02c66be976dd547bba816
6
+ metadata.gz: 4da3cd01812d9d8b8d8e28a4f35400cabcae133efd3192864eaa4820a9210c82dd78b35e353abaf74856d6f822fbf3f3eac94fa2c3dbe461f7b3167a129f7e6d
7
+ data.tar.gz: f4e261ec8b205d949058e8a124964250cfcef4a26ff93c59fa8fb269acc0e0b9c565e2b0bac2ae8071a92146d69a685b902bf11e6939282a27abe1c13cdbd1a9
data/Rakefile CHANGED
@@ -20,8 +20,4 @@ RDoc::Task.new(:rdoc) do |rdoc|
20
20
  rdoc.rdoc_files.include('lib/**/*.rb')
21
21
  end
22
22
 
23
-
24
-
25
-
26
23
  Bundler::GemHelper.install_tasks
27
-
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rswag/route_parser'
2
4
  require 'rails/generators'
3
5
 
4
6
  module Rspec
5
7
  class SwaggerGenerator < ::Rails::Generators::NamedBase
6
- source_root File.expand_path('../templates', __FILE__)
8
+ source_root File.expand_path('templates', __dir__)
7
9
 
8
10
  def setup
9
11
  @routes = Rswag::RouteParser.new(controller_path).routes
@@ -19,7 +19,11 @@ RSpec.describe '<%= controller_path %>', type: :request do
19
19
  <% end -%>
20
20
 
21
21
  after do |example|
22
- example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
22
+ example.metadata[:response][:content] = {
23
+ 'application/json' => {
24
+ example: JSON.parse(response.body, symbolize_names: true)
25
+ }
26
+ }
23
27
  end
24
28
  run_test!
25
29
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails/generators'
2
4
 
3
5
  module Rswag
4
6
  module Specs
5
-
6
7
  class InstallGenerator < Rails::Generators::Base
7
- source_root File.expand_path('../templates', __FILE__)
8
+ source_root File.expand_path('templates', __dir__)
8
9
 
9
10
  def add_swagger_helper
10
11
  template('swagger_helper.rb', 'spec/swagger_helper.rb')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails_helper'
2
4
 
3
5
  RSpec.configure do |config|
@@ -13,13 +15,29 @@ RSpec.configure do |config|
13
15
  # document below. You can override this behavior by adding a swagger_doc tag to the
14
16
  # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
15
17
  config.swagger_docs = {
16
- 'v1/swagger.json' => {
17
- swagger: '2.0',
18
+ 'v1/swagger.yaml' => {
19
+ openapi: '3.0.1',
18
20
  info: {
19
21
  title: 'API V1',
20
22
  version: 'v1'
21
23
  },
22
- paths: {}
24
+ paths: {},
25
+ servers: [
26
+ {
27
+ url: 'https://{defaultHost}',
28
+ variables: {
29
+ defaultHost: {
30
+ default: 'www.example.com'
31
+ }
32
+ }
33
+ }
34
+ ]
23
35
  }
24
36
  }
37
+
38
+ # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
39
+ # The swagger_docs configuration option has the filename including format in
40
+ # the key, this may want to be changed to avoid putting yaml in json files.
41
+ # Defaults to json. Accepts ':json' and ':yaml'.
42
+ config.swagger_format = :yaml
25
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rswag
2
4
  class RouteParser
3
5
  attr_reader :controller
@@ -9,7 +11,7 @@ module Rswag
9
11
  def routes
10
12
  ::Rails.application.routes.routes.select do |route|
11
13
  route.defaults[:controller] == controller
12
- end.reduce({}) do |tree, route|
14
+ end.each_with_object({}) do |route, tree|
13
15
  path = path_from(route)
14
16
  verb = verb_from(route)
15
17
  tree[path] ||= { params: params_from(route), actions: {} }
@@ -28,7 +30,7 @@ module Rswag
28
30
 
29
31
  def verb_from(route)
30
32
  verb = route.verb
31
- if verb.kind_of? String
33
+ if verb.is_a? String
32
34
  verb.downcase
33
35
  else
34
36
  verb.source.gsub(/[$^]/, '').downcase
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/core'
2
4
  require 'rswag/specs/example_group_helpers'
3
5
  require 'rswag/specs/example_helpers'
@@ -6,12 +8,12 @@ require 'rswag/specs/railtie' if defined?(Rails::Railtie)
6
8
 
7
9
  module Rswag
8
10
  module Specs
9
-
10
11
  # Extend RSpec with a swagger-based DSL
11
12
  ::RSpec.configure do |c|
12
13
  c.add_setting :swagger_root
13
14
  c.add_setting :swagger_docs
14
15
  c.add_setting :swagger_dry_run
16
+ c.add_setting :swagger_format
15
17
  c.extend ExampleGroupHelpers, type: :request
16
18
  c.include ExampleHelpers, type: :request
17
19
  end
@@ -1,8 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rswag
2
4
  module Specs
3
-
4
5
  class Configuration
5
-
6
6
  def initialize(rspec_config)
7
7
  @rspec_config = rspec_config
8
8
  end
@@ -12,6 +12,7 @@ module Rswag
12
12
  if @rspec_config.swagger_root.nil?
13
13
  raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
14
14
  end
15
+
15
16
  @rspec_config.swagger_root
16
17
  end
17
18
  end
@@ -21,6 +22,7 @@ module Rswag
21
22
  if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
22
23
  raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
23
24
  end
25
+
24
26
  @rspec_config.swagger_docs
25
27
  end
26
28
  end
@@ -31,11 +33,26 @@ module Rswag
31
33
  end
32
34
  end
33
35
 
36
+ def swagger_format
37
+ @swagger_format ||= begin
38
+ @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty?
39
+ raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
40
+
41
+ @rspec_config.swagger_format
42
+ end
43
+ end
44
+
34
45
  def get_swagger_doc(name)
35
46
  return swagger_docs.values.first if name.nil?
36
47
  raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
48
+
37
49
  swagger_docs[name]
38
50
  end
51
+
52
+ def get_swagger_doc_version(name)
53
+ doc = get_swagger_doc(name)
54
+ doc[:openapi] || doc[:swagger]
55
+ end
39
56
  end
40
57
 
41
58
  class ConfigurationError < StandardError; end
@@ -1,20 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rswag
2
4
  module Specs
3
5
  module ExampleGroupHelpers
4
-
5
- def path(template, metadata={}, &block)
6
+ def path(template, metadata = {}, &block)
6
7
  metadata[:path_item] = { template: template }
7
8
  describe(template, metadata, &block)
8
9
  end
9
10
 
10
- [ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb|
11
+ [:get, :post, :patch, :put, :delete, :head, :options, :trace].each do |verb|
11
12
  define_method(verb) do |summary, &block|
12
13
  api_metadata = { operation: { verb: verb, summary: summary } }
13
14
  describe(verb, api_metadata, &block)
14
15
  end
15
16
  end
16
17
 
17
- [ :operationId, :deprecated, :security ].each do |attr_name|
18
+ [:operationId, :deprecated, :security].each do |attr_name|
18
19
  define_method(attr_name) do |value|
19
20
  metadata[:operation][attr_name] = value
20
21
  end
@@ -23,13 +24,14 @@ module Rswag
23
24
  # NOTE: 'description' requires special treatment because ExampleGroup already
24
25
  # defines a method with that name. Provide an override that supports the existing
25
26
  # functionality while also setting the appropriate metadata if applicable
26
- def description(value=nil)
27
+ def description(value = nil)
27
28
  return super() if value.nil?
29
+
28
30
  metadata[:operation][:description] = value
29
31
  end
30
32
 
31
33
  # These are array properties - note the splat operator
32
- [ :tags, :consumes, :produces, :schemes ].each do |attr_name|
34
+ [:tags, :consumes, :produces, :schemes].each do |attr_name|
33
35
  define_method(attr_name) do |*value|
34
36
  metadata[:operation][attr_name] = value
35
37
  end
@@ -40,7 +42,7 @@ module Rswag
40
42
  attributes[:required] = true
41
43
  end
42
44
 
43
- if metadata.has_key?(:operation)
45
+ if metadata.key?(:operation)
44
46
  metadata[:operation][:parameters] ||= []
45
47
  metadata[:operation][:parameters] << attributes
46
48
  else
@@ -49,7 +51,7 @@ module Rswag
49
51
  end
50
52
  end
51
53
 
52
- def response(code, description, metadata={}, &block)
54
+ def response(code, description, metadata = {}, &block)
53
55
  metadata[:response] = { code: code, description: description }
54
56
  context(description, metadata, &block)
55
57
  end
@@ -60,6 +62,7 @@ module Rswag
60
62
 
61
63
  def header(name, attributes)
62
64
  metadata[:response][:headers] ||= {}
65
+
63
66
  metadata[:response][:headers][name] = attributes
64
67
  end
65
68
 
@@ -68,7 +71,11 @@ module Rswag
68
71
  # rspec-core ExampleGroup
69
72
  def examples(example = nil)
70
73
  return super() if example.nil?
71
- metadata[:response][:examples] = example
74
+
75
+ metadata[:response][:content] =
76
+ example.each_with_object({}) do |(mime, example_object), memo|
77
+ memo[mime] = { example: example_object }
78
+ end
72
79
  end
73
80
 
74
81
  def run_test!(&block)
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rswag/specs/request_factory'
2
4
  require 'rswag/specs/response_validator'
3
5
 
4
6
  module Rswag
5
7
  module Specs
6
8
  module ExampleHelpers
7
-
8
9
  def submit_request(metadata)
9
10
  request = RequestFactory.new.build_request(metadata, self)
10
11
 
@@ -19,10 +20,8 @@ module Rswag
19
20
  send(
20
21
  request[:verb],
21
22
  request[:path],
22
- {
23
- params: request[:payload],
24
- headers: request[:headers]
25
- }
23
+ params: request[:payload],
24
+ headers: request[:headers]
26
25
  )
27
26
  end
28
27
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json-schema'
2
4
 
3
5
  module Rswag
4
6
  module Specs
5
7
  class ExtendedSchema < JSON::Schema::Draft4
6
-
7
8
  def initialize
8
9
  super
9
10
  @attributes['type'] = ExtendedTypeAttribute
@@ -13,9 +14,9 @@ module Rswag
13
14
  end
14
15
 
15
16
  class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
17
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
18
+ return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
16
19
 
17
- def self.validate(current_schema, data, fragments, processor, validator, options={})
18
- return if data.nil? && current_schema.schema['x-nullable'] == true
19
20
  super
20
21
  end
21
22
  end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rswag
2
4
  module Specs
3
5
  class Railtie < ::Rails::Railtie
4
-
5
6
  rake_tasks do
6
- load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__)
7
+ load File.expand_path('../../tasks/rswag-specs_tasks.rake', __dir__)
7
8
  end
8
9
 
9
10
  generators do
10
- require 'generators/rspec/swagger/swagger_generator.rb'
11
+ require 'generators/rspec/swagger_generator.rb'
11
12
  end
12
13
  end
13
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/hash/slice'
2
4
  require 'active_support/core_ext/hash/conversions'
3
5
  require 'json'
@@ -5,7 +7,6 @@ require 'json'
5
7
  module Rswag
6
8
  module Specs
7
9
  class RequestFactory
8
-
9
10
  def initialize(config = ::Rswag::Specs.config)
10
11
  @config = config
11
12
  end
@@ -38,8 +39,8 @@ module Rswag
38
39
 
39
40
  def derive_security_params(metadata, swagger_doc)
40
41
  requirements = metadata[:operation][:security] || swagger_doc[:security] || []
41
- scheme_names = requirements.flat_map { |r| r.keys }
42
- schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
42
+ scheme_names = requirements.flat_map(&:keys)
43
+ schemes = security_version(scheme_names, swagger_doc)
43
44
 
44
45
  schemes.map do |scheme|
45
46
  param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
@@ -47,13 +48,55 @@ module Rswag
47
48
  end
48
49
  end
49
50
 
51
+ def security_version(scheme_names, swagger_doc)
52
+ if doc_version(swagger_doc).start_with?('2')
53
+ (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
54
+ else # Openapi3
55
+ if swagger_doc.key?(:securityDefinitions)
56
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)')
57
+ swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] }
58
+ swagger_doc.delete(:securityDefinitions)
59
+ end
60
+ components = swagger_doc[:components] || {}
61
+ (components[:securitySchemes] || {}).slice(*scheme_names).values
62
+ end
63
+ end
64
+
50
65
  def resolve_parameter(ref, swagger_doc)
51
- key = ref.sub('#/parameters/', '').to_sym
52
- definitions = swagger_doc[:parameters]
66
+ key = key_version(ref, swagger_doc)
67
+ definitions = definition_version(swagger_doc)
53
68
  raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
69
+
54
70
  definitions[key]
55
71
  end
56
72
 
73
+ def key_version(ref, swagger_doc)
74
+ if doc_version(swagger_doc).start_with?('2')
75
+ ref.sub('#/parameters/', '').to_sym
76
+ else # Openapi3
77
+ if ref.start_with?('#/parameters/')
78
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/')
79
+ ref.sub('#/parameters/', '').to_sym
80
+ else
81
+ ref.sub('#/components/parameters/', '').to_sym
82
+ end
83
+ end
84
+ end
85
+
86
+ def definition_version(swagger_doc)
87
+ if doc_version(swagger_doc).start_with?('2')
88
+ swagger_doc[:parameters]
89
+ else # Openapi3
90
+ if swagger_doc.key?(:parameters)
91
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)')
92
+ swagger_doc[:parameters]
93
+ else
94
+ components = swagger_doc[:components] || {}
95
+ components[:parameters]
96
+ end
97
+ end
98
+ end
99
+
57
100
  def add_verb(request, metadata)
58
101
  request[:verb] = metadata[:operation][:verb]
59
102
  end
@@ -61,21 +104,22 @@ module Rswag
61
104
  def add_path(request, metadata, swagger_doc, parameters, example)
62
105
  template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
63
106
 
64
- request[:path] = template.tap do |template|
107
+ request[:path] = template.tap do |path_template|
65
108
  parameters.select { |p| p[:in] == :path }.each do |p|
66
- template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
109
+ path_template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
67
110
  end
68
111
 
69
112
  parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
70
- template.concat(i == 0 ? '?' : '&')
71
- template.concat(build_query_string_part(p, example.send(p[:name])))
113
+ path_template.concat(i.zero? ? '?' : '&')
114
+ path_template.concat(build_query_string_part(p, example.send(p[:name])))
72
115
  end
73
116
  end
74
117
  end
75
118
 
76
119
  def build_query_string_part(param, value)
77
120
  name = param[:name]
78
- return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
121
+ type = param[:type] || param.dig(:schema, :type)
122
+ return "#{name}=#{value}" unless type&.to_sym == :array
79
123
 
80
124
  case param[:collectionFormat]
81
125
  when :ssv
@@ -94,43 +138,43 @@ module Rswag
94
138
  def add_headers(request, metadata, swagger_doc, parameters, example)
95
139
  tuples = parameters
96
140
  .select { |p| p[:in] == :header }
97
- .map { |p| [ p[:name], example.send(p[:name]).to_s ] }
141
+ .map { |p| [p[:name], example.send(p[:name]).to_s] }
98
142
 
99
143
  # Accept header
100
144
  produces = metadata[:operation][:produces] || swagger_doc[:produces]
101
145
  if produces
102
- accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first
103
- tuples << [ 'Accept', accept ]
146
+ accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
147
+ tuples << ['Accept', accept]
104
148
  end
105
149
 
106
150
  # Content-Type header
107
151
  consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
108
152
  if consumes
109
153
  content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
110
- tuples << [ 'Content-Type', content_type ]
154
+ tuples << ['Content-Type', content_type]
111
155
  end
112
156
 
113
157
  # Rails test infrastructure requires rackified headers
114
158
  rackified_tuples = tuples.map do |pair|
115
159
  [
116
160
  case pair[0]
117
- when 'Accept' then 'HTTP_ACCEPT'
118
- when 'Content-Type' then 'CONTENT_TYPE'
119
- when 'Authorization' then 'HTTP_AUTHORIZATION'
120
- else pair[0]
161
+ when 'Accept' then 'HTTP_ACCEPT'
162
+ when 'Content-Type' then 'CONTENT_TYPE'
163
+ when 'Authorization' then 'HTTP_AUTHORIZATION'
164
+ else pair[0]
121
165
  end,
122
166
  pair[1]
123
167
  ]
124
168
  end
125
169
 
126
- request[:headers] = Hash[ rackified_tuples ]
170
+ request[:headers] = Hash[rackified_tuples]
127
171
  end
128
172
 
129
173
  def add_payload(request, parameters, example)
130
174
  content_type = request[:headers]['CONTENT_TYPE']
131
175
  return if content_type.nil?
132
176
 
133
- if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type)
177
+ if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
134
178
  request[:payload] = build_form_payload(parameters, example)
135
179
  else
136
180
  request[:payload] = build_json_payload(parameters, example)
@@ -144,14 +188,18 @@ module Rswag
144
188
  # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
145
189
  tuples = parameters
146
190
  .select { |p| p[:in] == :formData }
147
- .map { |p| [ p[:name], example.send(p[:name]) ] }
148
- Hash[ tuples ]
191
+ .map { |p| [p[:name], example.send(p[:name])] }
192
+ Hash[tuples]
149
193
  end
150
194
 
151
195
  def build_json_payload(parameters, example)
152
196
  body_param = parameters.select { |p| p[:in] == :body }.first
153
197
  body_param ? example.send(body_param[:name]).to_json : nil
154
198
  end
199
+
200
+ def doc_version(doc)
201
+ doc[:openapi] || doc[:swagger] || '3'
202
+ end
155
203
  end
156
204
  end
157
205
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/hash/slice'
2
4
  require 'json-schema'
3
5
  require 'json'
@@ -6,7 +8,6 @@ require 'rswag/specs/extended_schema'
6
8
  module Rswag
7
9
  module Specs
8
10
  class ResponseValidator
9
-
10
11
  def initialize(config = ::Rswag::Specs.config)
11
12
  @config = config
12
13
  end
@@ -25,8 +26,8 @@ module Rswag
25
26
  expected = metadata[:response][:code].to_s
26
27
  if response.code != expected
27
28
  raise UnexpectedResponse,
28
- "Expected response code '#{response.code}' to match '#{expected}'\n" \
29
- "Response body: #{response.body}"
29
+ "Expected response code '#{response.code}' to match '#{expected}'\n" \
30
+ "Response body: #{response.body}"
30
31
  end
31
32
  end
32
33
 
@@ -41,12 +42,30 @@ module Rswag
41
42
  response_schema = metadata[:response][:schema]
42
43
  return if response_schema.nil?
43
44
 
45
+ version = @config.get_swagger_doc_version(metadata[:swagger_doc])
46
+ schemas = definitions_or_component_schemas(swagger_doc, version)
47
+
44
48
  validation_schema = response_schema
45
49
  .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
46
- .merge(swagger_doc.slice(:definitions))
50
+ .merge(schemas)
51
+
47
52
  errors = JSON::Validator.fully_validate(validation_schema, body)
48
53
  raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
49
54
  end
55
+
56
+ def definitions_or_component_schemas(swagger_doc, version)
57
+ if version.start_with?('2')
58
+ swagger_doc.slice(:definitions)
59
+ else # Openapi3
60
+ if swagger_doc.key?(:definitions)
61
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)')
62
+ swagger_doc.slice(:definitions)
63
+ else
64
+ components = swagger_doc[:components] || {}
65
+ { components: { schemas: components[:schemas] } }
66
+ end
67
+ end
68
+ end
50
69
  end
51
70
 
52
71
  class UnexpectedResponse < StandardError; end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/hash/deep_merge'
4
+ require 'rspec/core/formatters/base_text_formatter'
2
5
  require 'swagger_helper'
3
6
 
4
7
  module Rswag
5
8
  module Specs
6
- class SwaggerFormatter
9
+ class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter
7
10
 
8
11
  # NOTE: rspec 2.x support
9
12
  if RSPEC_VERSION > 2
@@ -19,25 +22,62 @@ module Rswag
19
22
 
20
23
  def example_group_finished(notification)
21
24
  # NOTE: rspec 2.x support
22
- if RSPEC_VERSION > 2
23
- metadata = notification.group.metadata
25
+ metadata = if RSPEC_VERSION > 2
26
+ notification.group.metadata
24
27
  else
25
- metadata = notification.metadata
28
+ notification.metadata
26
29
  end
27
30
 
28
- return unless metadata.has_key?(:response)
31
+ # !metadata[:document] won't work, since nil means we should generate
32
+ # docs.
33
+ return if metadata[:document] == false
34
+ return unless metadata.key?(:response)
35
+
29
36
  swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
37
+
38
+ unless doc_version(swagger_doc).start_with?('2')
39
+ # This is called multiple times per file!
40
+ # metadata[:operation] is also re-used between examples within file
41
+ # therefore be careful NOT to modify its content here.
42
+ upgrade_request_type!(metadata)
43
+ upgrade_servers!(swagger_doc)
44
+ upgrade_oauth!(swagger_doc)
45
+ upgrade_response_produces!(swagger_doc, metadata)
46
+ end
47
+
30
48
  swagger_doc.deep_merge!(metadata_to_swagger(metadata))
31
49
  end
32
50
 
33
- def stop(notification=nil)
51
+ def stop(_notification = nil)
34
52
  @config.swagger_docs.each do |url_path, doc|
53
+ unless doc_version(doc).start_with?('2')
54
+ doc[:paths]&.each_pair do |_k, v|
55
+ v.each_pair do |_verb, value|
56
+ is_hash = value.is_a?(Hash)
57
+ if is_hash && value.dig(:parameters)
58
+ schema_param = value.dig(:parameters)&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
59
+ mime_list = value.dig(:consumes) || doc[:consumes]
60
+ if value && schema_param && mime_list
61
+ value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content)
62
+ value[:requestBody][:required] = true if schema_param[:required]
63
+ mime_list.each do |mime|
64
+ value[:requestBody][:content][mime] = { schema: schema_param[:schema] }
65
+ end
66
+ end
67
+
68
+ value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
69
+ end
70
+ remove_invalid_operation_keys!(value)
71
+ end
72
+ end
73
+ end
74
+
35
75
  file_path = File.join(@config.swagger_root, url_path)
36
76
  dirname = File.dirname(file_path)
37
- FileUtils.mkdir_p dirname unless File.exists?(dirname)
77
+ FileUtils.mkdir_p dirname unless File.exist?(dirname)
38
78
 
39
79
  File.open(file_path, 'w') do |file|
40
- file.write(JSON.pretty_generate(doc))
80
+ file.write(pretty_generate(doc))
41
81
  end
42
82
 
43
83
  @output.puts "Swagger doc generated at #{file_path}"
@@ -46,22 +86,120 @@ module Rswag
46
86
 
47
87
  private
48
88
 
89
+ def pretty_generate(doc)
90
+ if @config.swagger_format == :yaml
91
+ clean_doc = yaml_prepare(doc)
92
+ YAML.dump(clean_doc)
93
+ else # config errors are thrown in 'def swagger_format', no throw needed here
94
+ JSON.pretty_generate(doc)
95
+ end
96
+ end
97
+
98
+ def yaml_prepare(doc)
99
+ json_doc = JSON.pretty_generate(doc)
100
+ JSON.parse(json_doc)
101
+ end
102
+
49
103
  def metadata_to_swagger(metadata)
50
104
  response_code = metadata[:response][:code]
51
- response = metadata[:response].reject { |k,v| k == :code }
105
+ response = metadata[:response].reject { |k, _v| k == :code }
52
106
 
53
107
  verb = metadata[:operation][:verb]
54
108
  operation = metadata[:operation]
55
- .reject { |k,v| k == :verb }
109
+ .reject { |k, _v| k == :verb }
56
110
  .merge(responses: { response_code => response })
57
111
 
58
112
  path_template = metadata[:path_item][:template]
59
113
  path_item = metadata[:path_item]
60
- .reject { |k,v| k == :template }
114
+ .reject { |k, _v| k == :template }
61
115
  .merge(verb => operation)
62
116
 
63
117
  { paths: { path_template => path_item } }
64
118
  end
119
+
120
+ def doc_version(doc)
121
+ doc[:openapi] || doc[:swagger] || '3'
122
+ end
123
+
124
+ def upgrade_response_produces!(swagger_doc, metadata)
125
+ # Accept header
126
+ mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces])
127
+ target_node = metadata[:response]
128
+ upgrade_content!(mime_list, target_node)
129
+ metadata[:response].delete(:schema)
130
+ end
131
+
132
+ def upgrade_content!(mime_list, target_node)
133
+ schema = target_node[:schema]
134
+ return if mime_list.empty? || schema.nil?
135
+ target_node[:content] ||= {}
136
+ target_node.merge!(content: {})
137
+
138
+ mime_list.each do |mime_type|
139
+ # TODO upgrade to have content-type specific schema
140
+ (target_node[:content][mime_type] ||= {}).merge!(schema: schema)
141
+ end
142
+ end
143
+
144
+ def upgrade_request_type!(metadata)
145
+ # No deprecation here as it seems valid to allow type as a shorthand
146
+ operation_nodes = metadata[:operation][:parameters] || []
147
+ path_nodes = metadata[:path_item][:parameters] || []
148
+ header_node = metadata[:response][:headers] || {}
149
+
150
+ (operation_nodes + path_nodes + [header_node]).each do |node|
151
+ if node && node[:type] && node[:schema].nil?
152
+ node[:schema] = { type: node[:type] }
153
+ node.delete(:type)
154
+ end
155
+ end
156
+ end
157
+
158
+ def upgrade_servers!(swagger_doc)
159
+ return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
160
+
161
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: schemes, host, and basePath are replaced in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)')
162
+
163
+ swagger_doc[:servers] = { urls: [] }
164
+ swagger_doc[:schemes].each do |scheme|
165
+ swagger_doc[:servers][:urls] << scheme + '://' + swagger_doc[:host] + swagger_doc[:basePath]
166
+ end
167
+
168
+ swagger_doc.delete(:schemes)
169
+ swagger_doc.delete(:host)
170
+ swagger_doc.delete(:basePath)
171
+ end
172
+
173
+ def upgrade_oauth!(swagger_doc)
174
+ # find flow in securitySchemes (securityDefinitions will have been re-written)
175
+ schemes = swagger_doc.dig(:components, :securitySchemes)
176
+ return unless schemes&.any? { |_k, v| v.key?(:flow) }
177
+
178
+ schemes.each do |name, v|
179
+ next unless v.key?(:flow)
180
+
181
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions flow is replaced in OpenAPI3! Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)")
182
+ flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
183
+ if flow == 'accessCode'
184
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions accessCode is replaced in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)")
185
+ flow = 'authorizationCode'
186
+ end
187
+ if flow == 'application'
188
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions application is replaced in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)")
189
+ flow = 'clientCredentials'
190
+ end
191
+ flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
192
+ a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
193
+ end
194
+ swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
195
+ end
196
+ end
197
+
198
+ def remove_invalid_operation_keys!(value)
199
+ is_hash = value.is_a?(Hash)
200
+ value.delete(:consumes) if is_hash && value.dig(:consumes)
201
+ value.delete(:produces) if is_hash && value.dig(:produces)
202
+ end
65
203
  end
66
204
  end
67
205
  end
@@ -1,18 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/core/rake_task'
2
4
 
3
5
  namespace :rswag do
4
6
  namespace :specs do
5
-
6
7
  desc 'Generate Swagger JSON files from integration specs'
7
8
  RSpec::Core::RakeTask.new('swaggerize') do |t|
8
- t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
9
+ t.pattern = ENV.fetch(
10
+ 'PATTERN',
11
+ 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
12
+ )
9
13
 
10
14
  # NOTE: rspec 2.x support
11
15
  if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run
12
- t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
16
+ t.rspec_opts = ['--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined']
13
17
  else
14
- t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ]
18
+ t.rspec_opts = ['--format Rswag::Specs::SwaggerFormatter', '--order defined']
15
19
  end
16
20
  end
17
21
  end
18
22
  end
23
+
24
+ task rswag: ['rswag:specs:swaggerize']
metadata CHANGED
@@ -1,14 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rswag-specs
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richie Morris
8
+ - Greg Myers
9
+ - Jay Danielian
8
10
  autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
- date: 2019-10-16 00:00:00.000000000 Z
13
+ date: 2021-01-28 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: activesupport
@@ -19,7 +21,7 @@ dependencies:
19
21
  version: '3.1'
20
22
  - - "<"
21
23
  - !ruby/object:Gem::Version
22
- version: '6.1'
24
+ version: '7.0'
23
25
  type: :runtime
24
26
  prerelease: false
25
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +31,7 @@ dependencies:
29
31
  version: '3.1'
30
32
  - - "<"
31
33
  - !ruby/object:Gem::Version
32
- version: '6.1'
34
+ version: '7.0'
33
35
  - !ruby/object:Gem::Dependency
34
36
  name: railties
35
37
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +41,7 @@ dependencies:
39
41
  version: '3.1'
40
42
  - - "<"
41
43
  - !ruby/object:Gem::Version
42
- version: '6.1'
44
+ version: '7.0'
43
45
  type: :runtime
44
46
  prerelease: false
45
47
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +51,7 @@ dependencies:
49
51
  version: '3.1'
50
52
  - - "<"
51
53
  - !ruby/object:Gem::Version
52
- version: '6.1'
54
+ version: '7.0'
53
55
  - !ruby/object:Gem::Dependency
54
56
  name: json-schema
55
57
  requirement: !ruby/object:Gem::Requirement
@@ -64,8 +66,9 @@ dependencies:
64
66
  - - "~>"
65
67
  - !ruby/object:Gem::Version
66
68
  version: '2.2'
67
- description: Simplify API integration testing with a succinct rspec DSL and generate
68
- Swagger files directly from your rspecs
69
+ description: 'Simplify API integration testing with a succinct rspec DSL and generate
70
+ OpenAPI specification files directly from your rspecs. More about the OpenAPI initiative
71
+ here: http://spec.openapis.org/'
69
72
  email:
70
73
  - domaindrivendev@gmail.com
71
74
  executables: []
@@ -91,7 +94,7 @@ files:
91
94
  - lib/rswag/specs/response_validator.rb
92
95
  - lib/rswag/specs/swagger_formatter.rb
93
96
  - lib/tasks/rswag-specs_tasks.rake
94
- homepage: https://github.com/domaindrivendev/rswag
97
+ homepage: https://github.com/rswag/rswag
95
98
  licenses:
96
99
  - MIT
97
100
  metadata: {}
@@ -110,10 +113,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
113
  - !ruby/object:Gem::Version
111
114
  version: '0'
112
115
  requirements: []
113
- rubyforge_project:
114
- rubygems_version: 2.7.7
116
+ rubygems_version: 3.0.6
115
117
  signing_key:
116
118
  specification_version: 4
117
- summary: A Swagger-based DSL for rspec-rails & accompanying rake task for generating
118
- Swagger files
119
+ summary: An OpenAPI-based (formerly called Swagger) DSL for rspec-rails & accompanying
120
+ rake task for generating OpenAPI specification files
119
121
  test_files: []