rswag-specs 2.1.1 → 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fa644707054d546e889410d5715437f34c6704d4a85d70c2d5eba14768da89e
4
- data.tar.gz: 9a175de33ad5ab07c111fbc03b475091ec5b51ea2a5c9be05c96fd46638a6f69
3
+ metadata.gz: 9834a553612943f7768facb91500a6a6a702efbbf1ed0f4d179caa4f66dea998
4
+ data.tar.gz: 1acf6ab7b297be895913f9b671959dbb68d86f1a916512d3fa576a0fa4f92994
5
5
  SHA512:
6
- metadata.gz: 5999a7bb27dbcb062e087e8ec1c0513c50f01921ebe6a24596013cdb25b9e3dc7e2e7f696df3fcea52667351e65919f79697e720d664bfe71e80c593db483d75
7
- data.tar.gz: 9467528f8a10bb435542b8726b4b677dbb77729606523596f4134b9df59190e7b80e7d7816124f3fadfeb97c9ed329a9dfcd89a9ec6b5a50e21baf857a1c66f4
6
+ metadata.gz: 62ea7c70b1dbad13ce834b2aac602259d712d581d50f49cc3c56b332f4067f4685ae1288db2b86de557149b4935b0fad1862f840493fc693f21aca69e3970f08
7
+ data.tar.gz: 5df7edd21cfcd4815b13b357ae20fbd56527ac45d81572d03bbdef15eb46985ef767c316087034ee273caf243d151b91b086091e7ceec17df5691459840f0ea8
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
data/lib/rswag/specs.rb CHANGED
@@ -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,9 +1,10 @@
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
@@ -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,119 @@ 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
+
136
+ target_node[:content] ||= {}
137
+ mime_list.each do |mime_type|
138
+ # TODO upgrade to have content-type specific schema
139
+ (target_node[:content][mime_type] ||= {}).merge!(schema: schema)
140
+ end
141
+ end
142
+
143
+ def upgrade_request_type!(metadata)
144
+ # No deprecation here as it seems valid to allow type as a shorthand
145
+ operation_nodes = metadata[:operation][:parameters] || []
146
+ path_nodes = metadata[:path_item][:parameters] || []
147
+ header_node = metadata[:response][:headers] || {}
148
+
149
+ (operation_nodes + path_nodes + [header_node]).each do |node|
150
+ if node && node[:type] && node[:schema].nil?
151
+ node[:schema] = { type: node[:type] }
152
+ node.delete(:type)
153
+ end
154
+ end
155
+ end
156
+
157
+ def upgrade_servers!(swagger_doc)
158
+ return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
159
+
160
+ ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: schemes, host, and basePath are replaced in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)')
161
+
162
+ swagger_doc[:servers] = { urls: [] }
163
+ swagger_doc[:schemes].each do |scheme|
164
+ swagger_doc[:servers][:urls] << scheme + '://' + swagger_doc[:host] + swagger_doc[:basePath]
165
+ end
166
+
167
+ swagger_doc.delete(:schemes)
168
+ swagger_doc.delete(:host)
169
+ swagger_doc.delete(:basePath)
170
+ end
171
+
172
+ def upgrade_oauth!(swagger_doc)
173
+ # find flow in securitySchemes (securityDefinitions will have been re-written)
174
+ schemes = swagger_doc.dig(:components, :securitySchemes)
175
+ return unless schemes&.any? { |_k, v| v.key?(:flow) }
176
+
177
+ schemes.each do |name, v|
178
+ next unless v.key?(:flow)
179
+
180
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions flow is replaced in OpenAPI3! Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)")
181
+ flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
182
+ if flow == 'accessCode'
183
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions accessCode is replaced in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)")
184
+ flow = 'authorizationCode'
185
+ end
186
+ if flow == 'application'
187
+ ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions application is replaced in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)")
188
+ flow = 'clientCredentials'
189
+ end
190
+ flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
191
+ a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
192
+ end
193
+ swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
194
+ end
195
+ end
196
+
197
+ def remove_invalid_operation_keys!(value)
198
+ is_hash = value.is_a?(Hash)
199
+ value.delete(:consumes) if is_hash && value.dig(:consumes)
200
+ value.delete(:produces) if is_hash && value.dig(:produces)
201
+ end
65
202
  end
66
203
  end
67
204
  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.1
4
+ version: 2.3.3
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-18 00:00:00.000000000 Z
13
+ date: 2021-02-07 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: []