rspec-openapi 0.1.0 → 0.18.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/release.yaml +24 -0
  4. data/.github/workflows/codeql-analysis.yml +39 -0
  5. data/.github/workflows/rubocop.yml +35 -0
  6. data/.github/workflows/test.yml +46 -0
  7. data/.rspec +2 -1
  8. data/.rubocop.yml +25 -0
  9. data/.rubocop_todo.yml +52 -0
  10. data/.simplecov_spawn.rb +16 -0
  11. data/CHANGELOG.md +287 -0
  12. data/Gemfile +28 -2
  13. data/README.md +267 -28
  14. data/Rakefile +8 -4
  15. data/bin/console +4 -3
  16. data/lib/rspec/openapi/components_updater.rb +98 -0
  17. data/lib/rspec/openapi/default_schema.rb +14 -2
  18. data/lib/rspec/openapi/extractors/hanami.rb +110 -0
  19. data/lib/rspec/openapi/extractors/rack.rb +31 -0
  20. data/lib/rspec/openapi/extractors/rails.rb +66 -0
  21. data/lib/rspec/openapi/extractors.rb +5 -0
  22. data/lib/rspec/openapi/hash_helper.rb +43 -0
  23. data/lib/rspec/openapi/key_transformer.rb +25 -0
  24. data/lib/rspec/openapi/minitest_hooks.rb +50 -0
  25. data/lib/rspec/openapi/record.rb +13 -3
  26. data/lib/rspec/openapi/record_builder.rb +65 -21
  27. data/lib/rspec/openapi/result_recorder.rb +63 -0
  28. data/lib/rspec/openapi/rspec_hooks.rb +21 -0
  29. data/lib/rspec/openapi/schema_builder.rb +166 -41
  30. data/lib/rspec/openapi/schema_cleaner.rb +129 -0
  31. data/lib/rspec/openapi/schema_file.rb +25 -3
  32. data/lib/rspec/openapi/schema_merger.rb +91 -21
  33. data/lib/rspec/openapi/schema_sorter.rb +35 -0
  34. data/lib/rspec/openapi/shared_hooks.rb +15 -0
  35. data/lib/rspec/openapi/version.rb +3 -1
  36. data/lib/rspec/openapi.rb +88 -2
  37. data/rspec-openapi.gemspec +19 -11
  38. data/scripts/rspec +11 -0
  39. data/scripts/rspec_with_simplecov +48 -0
  40. data/test.png +0 -0
  41. metadata +69 -15
  42. data/.travis.yml +0 -6
  43. data/lib/rspec/openapi/hooks.rb +0 -24
@@ -1,23 +1,44 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::SchemaBuilder = Object.new
2
4
  # @param [RSpec::OpenAPI::Record] record
3
5
  # @return [Hash]
4
6
  def build(record)
7
+ response = {
8
+ description: record.description,
9
+ }
10
+
11
+ response_headers = build_response_headers(record)
12
+ response[:headers] = response_headers unless response_headers.empty?
13
+
14
+ if record.response_body
15
+ disposition = normalize_content_disposition(record.response_content_disposition)
16
+
17
+ has_content = !normalize_content_type(record.response_content_type).nil?
18
+ if has_content
19
+ response[:content] = {
20
+ normalize_content_type(record.response_content_type) => {
21
+ schema: build_property(record.response_body, disposition: disposition),
22
+ example: response_example(record, disposition: disposition),
23
+ }.compact,
24
+ }
25
+ end
26
+ end
27
+
28
+ http_method = record.http_method.downcase
5
29
  {
6
30
  paths: {
7
- record.path => {
8
- record.method.downcase => {
9
- summary: "#{record.controller}##{record.action}",
31
+ normalize_path(record.path) => {
32
+ http_method => {
33
+ summary: record.summary,
34
+ tags: record.tags,
35
+ operationId: record.operation_id,
36
+ security: record.security,
37
+ deprecated: record.deprecated ? true : nil,
10
38
  parameters: build_parameters(record),
11
- requestBody: build_request_body(record),
39
+ requestBody: include_nil_request_body?(http_method) ? nil : build_request_body(record),
12
40
  responses: {
13
- record.status.to_s => {
14
- description: record.description,
15
- content: {
16
- normalize_content_type(record.response_content_type) => {
17
- schema: build_property(record.response_body),
18
- },
19
- },
20
- },
41
+ record.status.to_s => response,
21
42
  },
22
43
  }.compact,
23
44
  },
@@ -27,72 +48,140 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
27
48
 
28
49
  private
29
50
 
30
- def build_parameters(record)
31
- parameters = []
51
+ def include_nil_request_body?(http_method)
52
+ %w[delete get].include?(http_method)
53
+ end
32
54
 
33
- record.path_params.each do |key, value|
34
- next if %i[controller action].include?(key)
35
- parameters << {
36
- name: key.to_s,
55
+ def enrich_with_required_keys(obj)
56
+ obj[:required] = obj[:properties]&.keys || []
57
+ obj
58
+ end
59
+
60
+ def response_example(record, disposition:)
61
+ return nil if !example_enabled? || disposition
62
+
63
+ record.response_body
64
+ end
65
+
66
+ def example_enabled?
67
+ RSpec::OpenAPI.enable_example
68
+ end
69
+
70
+ def build_parameters(record)
71
+ path_params = record.path_params.map do |key, value|
72
+ {
73
+ name: build_parameter_name(key, value),
37
74
  in: 'path',
75
+ required: true,
38
76
  schema: build_property(try_cast(value)),
39
- }
77
+ example: (try_cast(value) if example_enabled?),
78
+ }.compact
40
79
  end
41
80
 
42
- record.query_params.each do |key, value|
43
- parameters << {
44
- name: key.to_s,
81
+ query_params = record.query_params.map do |key, value|
82
+ {
83
+ name: build_parameter_name(key, value),
45
84
  in: 'query',
85
+ required: record.required_request_params.include?(key),
86
+ schema: build_property(try_cast(value)),
87
+ example: (try_cast(value) if example_enabled?),
88
+ }.compact
89
+ end
90
+
91
+ header_params = record.request_headers.map do |key, value|
92
+ {
93
+ name: build_parameter_name(key, value),
94
+ in: 'header',
95
+ required: true,
46
96
  schema: build_property(try_cast(value)),
47
- }
97
+ example: (try_cast(value) if example_enabled?),
98
+ }.compact
48
99
  end
49
100
 
101
+ parameters = path_params + query_params + header_params
102
+
50
103
  return nil if parameters.empty?
104
+
51
105
  parameters
52
106
  end
53
107
 
108
+ def build_response_headers(record)
109
+ headers = {}
110
+
111
+ record.response_headers.each do |key, value|
112
+ headers[key] = {
113
+ schema: build_property(try_cast(value)),
114
+ }.compact
115
+ end
116
+
117
+ headers
118
+ end
119
+
120
+ def build_parameter_name(key, value)
121
+ key = key.to_s
122
+ if value.is_a?(Hash) && (value_keys = value.keys).size == 1
123
+ value_key = value_keys.first
124
+ build_parameter_name("#{key}[#{value_key}]", value[value_key])
125
+ else
126
+ key
127
+ end
128
+ end
129
+
54
130
  def build_request_body(record)
55
131
  return nil if record.request_content_type.nil?
56
- return nil if record.request_params.empty?
132
+ return nil if record.status >= 400
57
133
 
58
134
  {
59
135
  content: {
60
136
  normalize_content_type(record.request_content_type) => {
61
137
  schema: build_property(record.request_params),
62
- }
63
- }
138
+ example: (build_example(record.request_params) if example_enabled?),
139
+ }.compact,
140
+ },
64
141
  }
65
142
  end
66
143
 
67
- def build_property(value)
68
- property = { type: build_type(value) }
144
+ def build_property(value, disposition: nil)
145
+ property = build_type(value, disposition)
146
+
69
147
  case value
70
148
  when Array
71
- property[:items] = build_property(value.first)
149
+ property[:items] = if value.empty?
150
+ {} # unknown
151
+ else
152
+ build_property(value.first)
153
+ end
72
154
  when Hash
73
155
  property[:properties] = {}.tap do |properties|
74
156
  value.each do |key, v|
75
157
  properties[key] = build_property(v)
76
158
  end
77
159
  end
160
+ property = enrich_with_required_keys(property)
78
161
  end
79
162
  property
80
163
  end
81
164
 
82
- def build_type(value)
165
+ def build_type(value, disposition)
166
+ return { type: 'string', format: 'binary' } if disposition
167
+
83
168
  case value
84
169
  when String
85
- 'string'
170
+ { type: 'string' }
86
171
  when Integer
87
- 'integer'
172
+ { type: 'integer' }
173
+ when Float
174
+ { type: 'number', format: 'float' }
88
175
  when TrueClass, FalseClass
89
- 'boolean'
176
+ { type: 'boolean' }
90
177
  when Array
91
- 'array'
178
+ { type: 'array' }
92
179
  when Hash
93
- 'object'
180
+ { type: 'object' }
181
+ when ActionDispatch::Http::UploadedFile
182
+ { type: 'string', format: 'binary' }
94
183
  when NilClass
95
- 'null'
184
+ { nullable: true }
96
185
  else
97
186
  raise NotImplementedError, "type detection is not implemented for: #{value.inspect}"
98
187
  end
@@ -100,14 +189,50 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
100
189
 
101
190
  # Convert an always-String param to an appropriate type
102
191
  def try_cast(value)
103
- begin
104
- Integer(value)
105
- rescue TypeError, ArgumentError
106
- value
192
+ Integer(value)
193
+ rescue TypeError, ArgumentError
194
+ value
195
+ end
196
+
197
+ def build_example(value)
198
+ return nil if value.nil?
199
+
200
+ value = value.dup
201
+ adjust_params(value)
202
+ end
203
+
204
+ def adjust_params(value)
205
+ value.each do |key, v|
206
+ case v
207
+ when ActionDispatch::Http::UploadedFile
208
+ value[key] = v.original_filename
209
+ when Hash
210
+ adjust_params(v)
211
+ when Array
212
+ result = v.map do |item|
213
+ case item
214
+ when ActionDispatch::Http::UploadedFile
215
+ item.original_filename
216
+ when Hash
217
+ adjust_params(item)
218
+ else
219
+ item
220
+ end
221
+ end
222
+ value[key] = result
223
+ end
107
224
  end
108
225
  end
109
226
 
227
+ def normalize_path(path)
228
+ path.gsub(%r{/:([^:/]+)}, '/{\1}')
229
+ end
230
+
110
231
  def normalize_content_type(content_type)
111
- content_type.sub(/;.+\z/, '')
232
+ content_type&.sub(/;.+\z/, '')
233
+ end
234
+
235
+ def normalize_content_disposition(content_disposition)
236
+ content_disposition&.sub(/;.+\z/, '')
112
237
  end
113
238
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For Ruby 3.0+
4
+ require 'set'
5
+
6
+ require_relative 'hash_helper'
7
+
8
+ class << RSpec::OpenAPI::SchemaCleaner = Object.new
9
+ # Cleanup the properties, of component schemas, that exists in the base but not in the spec.
10
+ #
11
+ # @param [Hash] base
12
+ # @param [Hash] spec
13
+ def cleanup_components_schemas!(base, spec)
14
+ cleanup_hash!(base, spec, 'components.schemas.*')
15
+ cleanup_hash!(base, spec, 'components.schemas.*.properties.*')
16
+ end
17
+
18
+ # Cleanup specific elements that exists in the base but not in the spec
19
+ #
20
+ # @param [Hash] base
21
+ # @param [Hash] spec
22
+ def cleanup!(base, spec)
23
+ # cleanup URLs
24
+ cleanup_hash!(base, spec, 'paths.*')
25
+
26
+ # cleanup HTTP methods
27
+ cleanup_hash!(base, spec, 'paths.*.*')
28
+
29
+ # cleanup parameters
30
+ cleanup_array!(base, spec, 'paths.*.*.parameters', %i[name in])
31
+
32
+ # cleanup requestBody
33
+ cleanup_hash!(base, spec, 'paths.*.*.requestBody.content.application/json.schema.properties.*')
34
+ cleanup_hash!(base, spec, 'paths.*.*.requestBody.content.application/json.example.*')
35
+
36
+ # cleanup responses
37
+ cleanup_hash!(base, spec, 'paths.*.*.responses.*.content.application/json.schema.properties.*')
38
+ cleanup_hash!(base, spec, 'paths.*.*.responses.*.content.application/json.example.*')
39
+ base
40
+ end
41
+
42
+ def cleanup_conflicting_security_parameters!(base)
43
+ security_schemes = base.dig(:components, :securitySchemes) || {}
44
+
45
+ return if security_schemes.empty?
46
+
47
+ paths_to_security_definitions = RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'security')
48
+
49
+ paths_to_security_definitions.each do |path|
50
+ parent_path_definition = base.dig(*path.take(path.length - 1))
51
+
52
+ security_schemes.each do |security_scheme_name, security_scheme|
53
+ remove_parameters_conflicting_with_security_sceheme!(
54
+ parent_path_definition, security_scheme, security_scheme_name,
55
+ )
56
+ end
57
+ end
58
+ end
59
+
60
+ def cleanup_empty_required_array!(base)
61
+ paths_to_objects = [
62
+ *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties'),
63
+ *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'properties'),
64
+ ]
65
+ paths_to_objects.each do |path|
66
+ parent = base.dig(*path.take(path.length - 1))
67
+ # "required" array must not be present if empty
68
+ parent.delete(:required) if parent[:required] && parent[:required].empty?
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def remove_parameters_conflicting_with_security_sceheme!(path_definition, security_scheme, security_scheme_name)
75
+ return unless path_definition[:security]
76
+ return unless path_definition[:parameters]
77
+ return unless path_definition.dig(:security, 0).keys.include?(security_scheme_name)
78
+
79
+ path_definition[:parameters].reject! do |parameter|
80
+ parameter[:in] == security_scheme[:in] && # same location (ie. header)
81
+ parameter[:name] == security_scheme[:name] # same name (ie. AUTHORIZATION)
82
+ end
83
+ path_definition.delete(:parameters) if path_definition[:parameters].empty?
84
+ end
85
+
86
+ def cleanup_array!(base, spec, selector, fields_for_identity = [])
87
+ marshal = lambda do |obj|
88
+ Marshal.dump(slice(obj, fields_for_identity))
89
+ end
90
+
91
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
92
+ target_array = base.dig(*paths)
93
+ spec_array = spec.dig(*paths)
94
+ next unless target_array.is_a?(Array) && spec_array.is_a?(Array)
95
+
96
+ spec_identities = Set.new(spec_array.map(&marshal))
97
+ target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
98
+ target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
99
+ # Keep the last duplicate to produce the result stably
100
+ deduplicated = target_array.reverse.uniq { |param| slice(param, fields_for_identity) }.reverse
101
+ target_array.replace(deduplicated)
102
+ end
103
+ base
104
+ end
105
+
106
+ def cleanup_hash!(base, spec, selector)
107
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
108
+ exist_in_base = !base.dig(*paths).nil?
109
+ not_in_spec = spec.dig(*paths).nil?
110
+ if exist_in_base && not_in_spec
111
+ if paths.size == 1
112
+ base.delete(paths.last)
113
+ else
114
+ parent_node = base.dig(*paths[0..-2])
115
+ parent_node.delete(paths.last)
116
+ end
117
+ end
118
+ end
119
+ base
120
+ end
121
+
122
+ def slice(obj, fields_for_identity)
123
+ if fields_for_identity.any?
124
+ obj.slice(*fields_for_identity)
125
+ else
126
+ obj
127
+ end
128
+ end
129
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
  require 'yaml'
5
+ require 'json'
3
6
 
4
7
  # TODO: Support JSON
5
8
  class RSpec::OpenAPI::SchemaFile
@@ -12,7 +15,7 @@ class RSpec::OpenAPI::SchemaFile
12
15
  spec = read
13
16
  block.call(spec)
14
17
  ensure
15
- write(spec)
18
+ write(RSpec::OpenAPI::KeyTransformer.stringify(spec))
16
19
  end
17
20
 
18
21
  private
@@ -20,12 +23,31 @@ class RSpec::OpenAPI::SchemaFile
20
23
  # @return [Hash]
21
24
  def read
22
25
  return {} unless File.exist?(@path)
23
- YAML.load(File.read(@path))
26
+
27
+ RSpec::OpenAPI::KeyTransformer.symbolize(YAML.safe_load(File.read(@path))) # this can also parse JSON
24
28
  end
25
29
 
26
30
  # @param [Hash] spec
27
31
  def write(spec)
28
32
  FileUtils.mkdir_p(File.dirname(@path))
29
- File.write(@path, YAML.dump(spec))
33
+ output =
34
+ if json?
35
+ JSON.pretty_generate(spec)
36
+ else
37
+ prepend_comment(YAML.dump(spec))
38
+ end
39
+ File.write(@path, output)
40
+ end
41
+
42
+ def prepend_comment(content)
43
+ return content if RSpec::OpenAPI.comment.nil?
44
+
45
+ comment = RSpec::OpenAPI.comment.dup
46
+ comment << "\n" unless comment.end_with?("\n")
47
+ "#{comment.gsub(/^/, '# ').gsub(/^# \n/, "#\n")}#{content}"
48
+ end
49
+
50
+ def json?
51
+ File.extname(@path) == '.json'
30
52
  end
31
53
  end
@@ -1,37 +1,107 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::SchemaMerger = Object.new
2
4
  # @param [Hash] base
3
5
  # @param [Hash] spec
4
- def reverse_merge!(base, spec)
5
- spec = normalize_keys(spec)
6
- deep_reverse_merge!(base, spec)
6
+ def merge!(base, spec)
7
+ spec = RSpec::OpenAPI::KeyTransformer.symbolize(spec)
8
+ base.replace(RSpec::OpenAPI::KeyTransformer.symbolize(base))
9
+ merge_schema!(base, spec)
7
10
  end
8
11
 
9
12
  private
10
13
 
11
- def normalize_keys(spec)
12
- case spec
13
- when Hash
14
- spec.map do |key, value|
15
- [key.to_s, normalize_keys(value)]
16
- end.to_h
17
- when Array
18
- spec.map { |s| normalize_keys(s) }
19
- else
20
- spec
14
+ # Not doing `base.replace(deep_merge(base, spec))` to preserve key orders.
15
+ # Also this needs to be aware of OpenAPI details because a Hash-like structure
16
+ # may be an array whose Hash elements have a key name.
17
+ #
18
+ # TODO: Should we probably force-merge `summary` regardless of manual modifications?
19
+ def merge_schema!(base, spec)
20
+ if (options = base[:oneOf])
21
+ merge_closest_match!(options, spec)
22
+
23
+ return base
21
24
  end
22
- end
23
25
 
24
- # Not doing `base.replace(deep_merge(base, spec))` to preserve key orders
25
- # TODO: Perform more intelligent merges like rerouting edits / merging types
26
- # Should we probably force-merge `summary` regardless of manual modifications?
27
- def deep_reverse_merge!(base, spec)
28
26
  spec.each do |key, value|
29
27
  if base[key].is_a?(Hash) && value.is_a?(Hash)
30
- deep_reverse_merge!(base[key], value)
31
- elsif !base.key?(key)
32
- base[key] = value
28
+ merge_schema!(base[key], value) unless base[key].key?(:$ref)
29
+ elsif base[key].is_a?(Array) && value.is_a?(Array)
30
+ # parameters need to be merged as if `name` and `in` were the Hash keys.
31
+ merge_arrays(base, key, value)
32
+ else
33
+ # do not ADD `properties` or `required` fields if `additionalProperties` field is present
34
+ base[key] = value unless base.key?(:additionalProperties) && %i[properties required].include?(key)
33
35
  end
34
36
  end
35
37
  base
36
38
  end
39
+
40
+ def merge_arrays(base, key, value)
41
+ base[key] = case key
42
+ when :parameters
43
+ merge_parameters(base, key, value)
44
+ when :required
45
+ # Preserve properties that appears in all test cases
46
+ value & base[key]
47
+ else
48
+ # last one wins
49
+ value
50
+ end
51
+ end
52
+
53
+ def merge_parameters(base, key, value)
54
+ all_parameters = value | base[key]
55
+
56
+ unique_base_parameters = build_unique_params(base, key)
57
+
58
+ all_parameters = all_parameters.map do |parameter|
59
+ base_parameter = unique_base_parameters[[parameter[:name], parameter[:in]]] || {}
60
+ base_parameter ? base_parameter.merge(parameter) : parameter
61
+ end
62
+
63
+ all_parameters.uniq! { |param| param.slice(:name, :in) }
64
+ base[key] = all_parameters
65
+ end
66
+
67
+ def build_unique_params(base, key)
68
+ base[key].each_with_object({}) do |parameter, hash|
69
+ hash[[parameter[:name], parameter[:in]]] = parameter
70
+ end
71
+ end
72
+
73
+ SIMILARITY_THRESHOLD = 0.5
74
+
75
+ def merge_closest_match!(options, spec)
76
+ score, option = options.map { |option| [similarity(option, spec), option] }.max_by(&:first)
77
+
78
+ return if option&.key?(:$ref)
79
+
80
+ if score.to_f > SIMILARITY_THRESHOLD
81
+ merge_schema!(option, spec)
82
+ else
83
+ options.push(spec)
84
+ end
85
+ end
86
+
87
+ def similarity(first, second)
88
+ return 1 if first == second
89
+
90
+ score =
91
+ case [first.class, second.class]
92
+ when [Array, Array]
93
+ (first & second).size / [first.size, second.size].max.to_f
94
+ when [Hash, Hash]
95
+ return 1 if first.merge(second).key?(:$ref)
96
+
97
+ intersection = first.keys & second.keys
98
+ total_size = [first.size, second.size].max.to_f
99
+
100
+ intersection.sum { |key| similarity(first[key], second[key]) } / total_size
101
+ else
102
+ 0
103
+ end
104
+
105
+ score.finite? ? score : 0
106
+ end
37
107
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class << RSpec::OpenAPI::SchemaSorter = Object.new
4
+ # Sort some unpredictably ordered properties in a lexicographical manner to make the order more predictable.
5
+ #
6
+ # @param [Hash|Array]
7
+ def deep_sort!(spec)
8
+ # paths
9
+ deep_sort_by_selector!(spec, 'paths')
10
+
11
+ # methods
12
+ deep_sort_by_selector!(spec, 'paths.*')
13
+
14
+ # response status code
15
+ deep_sort_by_selector!(spec, 'paths.*.*.responses')
16
+
17
+ # content-type
18
+ deep_sort_by_selector!(spec, 'paths.*.*.responses.*.content')
19
+ end
20
+
21
+ private
22
+
23
+ # @param [Hash] base
24
+ # @param [String] selector
25
+ def deep_sort_by_selector!(base, selector)
26
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
27
+ deep_sort_hash!(base.dig(*paths))
28
+ end
29
+ end
30
+
31
+ def deep_sort_hash!(hash)
32
+ sorted = hash.entries.sort_by { |k, _| k.to_s }.to_h.transform_keys(&:to_sym)
33
+ hash.replace(sorted)
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedHooks
4
+ def self.find_extractor
5
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
6
+ RSpec::OpenAPI::Extractors::Rails
7
+ elsif defined?(Hanami) && Hanami.respond_to?(:app) && Hanami.app?
8
+ RSpec::OpenAPI::Extractors::Hanami
9
+ # elsif defined?(Roda)
10
+ # some Roda extractor
11
+ else
12
+ RSpec::OpenAPI::Extractors::Rack
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RSpec
2
4
  module OpenAPI
3
- VERSION = '0.1.0'
5
+ VERSION = '0.18.4'
4
6
  end
5
7
  end