openapi-ruby 2.3.0 → 2.4.0

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: 398dab24d8afcf002fff3be85c3e47c2b83688904d2b3577e071ad790398caf0
4
- data.tar.gz: 071ad3e3c2b9d15e6f4aa60fcf09f8fef2260f43e35e9c6a69af04071da2d6a6
3
+ metadata.gz: 625d042fe294eeec35636e245418a38ab1cdefb13412fb1cfd355429a90ead83
4
+ data.tar.gz: 354e09c385f606421e191f05bccfef6a1cefac42be17485f3eeb4bf38ccf3285
5
5
  SHA512:
6
- metadata.gz: 7745b15a5b56b12b9a7c2b24ddfe761d37faea4835114722960dbc575bd2ae386780284a2791a1b57d08374cad5a8f5079ff410cefcbedce521c719bc892ab81
7
- data.tar.gz: a6baddb8b5850f6b273506f04fa9e8917338431c3d08ab698f710fa791b7c99e4f4ad5b861c298ca08b9b156342606ca760efb7720d7d1a735f6197faee4807d
6
+ metadata.gz: de6d641c194b48f03e20c877a9c9ab787eaf20dd88fe8bf952bb2c22963e98b060f31aea0d5d57321a99d6ffd91fe9b80805b3bbeb4274d17f085b7481747588
7
+ data.tar.gz: f9790faa37a74d230addcf0fdaaa346454481c63791e73efa138aa1bdc212cf08111dee1eb1cfcc6b1b867d90189cf4e76bea4305d27474f7098c7214cbebe00
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi_ruby"
4
+ require "cgi"
5
+ require "uri"
4
6
 
5
7
  module OpenapiRuby
6
8
  module Adapters
@@ -36,45 +38,64 @@ module OpenapiRuby
36
38
  response_ctx = operation.responses[expected_status.to_s]
37
39
  raise OpenapiRuby::Error, "No response #{expected_status} defined for #{method.upcase}" unless response_ctx
38
40
 
39
- # Build the request path
40
- path = expand_path(context.path_template, params.merge(path_params))
41
+ # Build the request path with base path from schema server URL
42
+ base_path = resolve_base_path(context.schema_name)
43
+ path = "#{base_path}#{expand_path(context.path_template, params.merge(path_params))}"
41
44
 
42
- # Execute the request
43
- request_params = body || params.reject { |k, _| path_param_names(context).include?(k.to_s) }
44
- request_headers = headers.dup
45
+ # Resolve security scheme parameters
46
+ security_params = resolve_security_params(operation, context.schema_name)
47
+ security_params.each do |param|
48
+ val = params[param[:name].to_sym] || params[param[:name]]
49
+ next if val.nil?
45
50
 
46
- if body
47
- content_type = request_headers["Content-Type"] || context.operations[method.to_s]&.instance_variable_get(:@consumes_list)&.first
51
+ case param[:in].to_s
52
+ when "header" then headers[param[:name]] = val
53
+ when "query" then params[param[:name]] = val
54
+ when "cookie" then headers["Cookie"] = "#{param[:name]}=#{val}"
55
+ end
56
+ end
48
57
 
49
- if content_type&.include?("form-data") || content_type&.include?("x-www-form-urlencoded")
50
- request_params = body
51
- request_headers["Content-Type"] ||= content_type
58
+ # Default Accept header for API requests
59
+ headers["Accept"] ||= "application/json"
60
+
61
+ # Build query params (exclude path params)
62
+ query_params = params.reject { |k, _| path_param_names(context).include?(k.to_s) }
63
+
64
+ # Execute the request
65
+ if body
66
+ content_type = operation.request_body_definition&.dig("content")&.keys&.first || "application/json"
67
+ request_args = if content_type.include?("form-data") || content_type.include?("x-www-form-urlencoded")
68
+ {params: body, headers: headers}
52
69
  else
53
- request_params = body.is_a?(String) ? body : body.to_json
54
- request_headers["Content-Type"] ||= content_type || "application/json"
70
+ {
71
+ params: body.is_a?(String) ? body : body.to_json,
72
+ headers: headers.merge("Content-Type" => content_type)
73
+ }
74
+ end
75
+ # Append query params to path when body is present
76
+ if query_params.any?
77
+ query_string = query_params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join("&")
78
+ path = "#{path}?#{query_string}"
55
79
  end
80
+ else
81
+ request_args = {params: query_params, headers: headers}
56
82
  end
57
83
 
58
- send_args = {params: request_params}
59
- send_args[:headers] = request_headers if request_headers.any?
60
-
61
- send(method, path, **send_args)
84
+ send(method, path, **request_args)
62
85
 
63
86
  # Validate response
64
- if OpenapiRuby.configuration.validate_responses_in_tests
65
- assert_equal expected_status, response.status,
66
- "Expected status #{expected_status}, got #{response.status}"
67
-
68
- if response_ctx.schema_definition
69
- validator = Testing::ResponseValidator.new
70
- body_data = parse_response_body
71
- errors = validator.validate(
72
- response_body: body_data,
73
- status_code: response.status,
74
- response_context: response_ctx
75
- )
76
- assert errors.empty?, "Response validation failed:\n#{errors.join("\n")}"
77
- end
87
+ assert_equal expected_status, response.status,
88
+ "Expected status #{expected_status}, got #{response.status}\nResponse body: #{response.body}"
89
+
90
+ if OpenapiRuby.configuration.validate_responses_in_tests && response_ctx.schema_definition
91
+ validator = Testing::ResponseValidator.new
92
+ body_data = parse_response_body
93
+ errors = validator.validate(
94
+ response_body: body_data,
95
+ status_code: response.status,
96
+ response_context: response_ctx
97
+ )
98
+ assert errors.empty?, "Response validation failed:\n#{errors.join("\n")}"
78
99
  end
79
100
 
80
101
  # Execute additional assertions
@@ -94,10 +115,8 @@ module OpenapiRuby
94
115
  next false unless ctx.operations.key?(method.to_s)
95
116
 
96
117
  if has_path_params
97
- # Pick the context that has path parameter placeholders
98
118
  ctx.path_template.include?("{")
99
119
  else
100
- # Pick the context without path parameter placeholders
101
120
  !ctx.path_template.include?("{")
102
121
  end
103
122
  end
@@ -115,6 +134,52 @@ module OpenapiRuby
115
134
  context.path_parameters.map { |p| p["name"] }
116
135
  end
117
136
 
137
+ def resolve_base_path(schema_name)
138
+ return "" unless schema_name
139
+
140
+ config = OpenapiRuby.configuration
141
+ schema_config = config.schemas[schema_name.to_sym] || config.schemas[schema_name.to_s]
142
+ return "" unless schema_config
143
+
144
+ server_url = schema_config.dig(:servers, 0, :url) || schema_config.dig("servers", 0, "url")
145
+ return "" unless server_url
146
+
147
+ URI.parse(server_url).path.chomp("/")
148
+ rescue URI::InvalidURIError
149
+ ""
150
+ end
151
+
152
+ def resolve_security_params(operation, schema_name)
153
+ security_list = operation.instance_variable_get(:@security_list)
154
+ return [] unless security_list
155
+
156
+ config = OpenapiRuby.configuration
157
+ schema_config = config.schemas[schema_name.to_sym] || config.schemas[schema_name.to_s]
158
+ return [] unless schema_config
159
+
160
+ security_schemes = schema_config.dig(:components, :securitySchemes) ||
161
+ schema_config.dig("components", "securitySchemes") || {}
162
+
163
+ if security_schemes.empty?
164
+ loader = Components::Loader.new(scope: schema_name.to_s.tr("/", "_").to_sym)
165
+ security_schemes = loader.security_schemes
166
+ end
167
+
168
+ scheme_names = security_list.flat_map { |s| s.is_a?(Hash) ? s.keys.map(&:to_s) : [] }
169
+
170
+ scheme_names.filter_map do |name|
171
+ scheme = security_schemes[name] || security_schemes[name.to_sym]
172
+ next unless scheme
173
+
174
+ type = scheme[:type] || scheme["type"]
175
+ if type.to_s == "apiKey"
176
+ {name: (scheme[:name] || scheme["name"]).to_s, in: (scheme[:in] || scheme["in"]).to_s}
177
+ else
178
+ {name: "Authorization", in: "header"}
179
+ end
180
+ end.uniq { |p| [p[:name], p[:in]] }
181
+ end
182
+
118
183
  def parse_response_body
119
184
  return nil if response.body.empty?
120
185
 
@@ -1,256 +1,256 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi_ruby"
4
+ require "cgi"
5
+ require "uri"
4
6
 
5
7
  module OpenapiRuby
6
8
  module Adapters
7
9
  module RSpec
10
+ # Class-level DSL methods extended onto :openapi example groups.
11
+ # All methods are inherited by nested describe/context/it_behaves_like blocks.
12
+ # Data is stored in RSpec metadata which propagates to child groups.
8
13
  module ExampleGroupHelpers
9
14
  def path(template, &block)
10
15
  schema_name = metadata[:openapi_schema_name]
11
16
  context = DSL::Context.new(template, schema_name: schema_name)
12
17
 
13
18
  describe template do
14
- # Store context reference on the example group metadata
15
19
  metadata[:openapi_path_context] = context
16
-
17
- # Evaluate the block which defines operations via get/post/etc.
18
- # We need a proxy that captures DSL calls and maps them to RSpec describe blocks
19
- proxy = PathProxy.new(self, context)
20
- proxy.instance_eval(&block) if block
21
-
22
- # Register the context for spec generation
20
+ instance_eval(&block) if block
23
21
  DSL::MetadataStore.register(context)
24
22
  end
25
23
  end
26
- end
27
-
28
- class PathProxy
29
- def initialize(example_group, context)
30
- @example_group = example_group
31
- @context = context
32
- end
33
-
34
- def parameter(attributes = {})
35
- @context.parameter(attributes)
36
- end
37
24
 
38
25
  DSL::Context::HTTP_METHODS.each do |method|
39
26
  define_method(method) do |summary = nil, &block|
27
+ path_ctx = metadata[:openapi_path_context]
40
28
  op_context = DSL::OperationContext.new(method, summary)
41
- # Copy path-level parameters
42
- @context.path_parameters.each { |p| op_context.parameter(p) }
43
- @context.operations[method.to_s] = op_context
44
-
45
- @example_group.describe "#{method.to_s.upcase} #{@context.path_template}" do
46
- # Evaluate operation-level DSL
47
- op_proxy = OperationProxy.new(self, op_context, @context)
48
- op_proxy.instance_eval(&block) if block
29
+ path_ctx.path_parameters.each { |p| op_context.parameter(p) }
30
+ path_ctx.operations[method.to_s] = op_context
31
+
32
+ describe "#{method.to_s.upcase} #{path_ctx.path_template}" do
33
+ metadata[:openapi_operation] = op_context
34
+ instance_eval(&block) if block
49
35
  end
50
36
  end
51
37
  end
52
38
 
53
- private
54
-
55
- # Forward missing methods to the example group for non-DSL calls
56
- def method_missing(name, ...)
57
- if @example_group.respond_to?(name)
58
- @example_group.send(name, ...)
59
- else
60
- super
39
+ def parameter(attributes = {})
40
+ if metadata[:openapi_operation]
41
+ metadata[:openapi_operation].parameter(attributes)
42
+ elsif metadata[:openapi_path_context]
43
+ metadata[:openapi_path_context].parameter(attributes)
61
44
  end
62
45
  end
63
46
 
64
- def respond_to_missing?(name, include_private = false)
65
- @example_group.respond_to?(name, include_private) || super
47
+ %i[tags operationId deprecated security].each do |attr_name|
48
+ define_method(attr_name) do |value|
49
+ metadata[:openapi_operation]&.send(attr_name, value)
50
+ end
66
51
  end
67
- end
68
52
 
69
- class OperationProxy
70
- def initialize(example_group, operation_context, path_context)
71
- @example_group = example_group
72
- @operation = operation_context
73
- @path_context = path_context
53
+ def description(value = nil)
54
+ return super() if value.nil?
55
+ metadata[:openapi_operation]&.description(value)
74
56
  end
75
57
 
76
- %i[tags operationId description deprecated consumes produces security
77
- parameter request_body request_body_example].each do |method|
78
- define_method(method) do |*args, **kwargs, &block|
79
- if kwargs.empty?
80
- @operation.send(method, *args, &block)
81
- else
82
- @operation.send(method, *args, **kwargs, &block)
83
- end
84
- end
58
+ def consumes(*content_types)
59
+ metadata[:openapi_operation]&.consumes(*content_types)
85
60
  end
86
61
 
87
- def response(status_code, description, hidden: false, &block)
88
- response_ctx = @operation.response(status_code, description, hidden: hidden)
89
- operation = @operation
90
-
91
- @example_group.context "response #{status_code} #{description}" do
92
- metadata[:openapi_operation] = operation
93
- metadata[:openapi_response] = response_ctx
94
-
95
- # Evaluate response-level DSL
96
- resp_proxy = ResponseProxy.new(self, response_ctx)
97
- resp_proxy.instance_eval(&block) if block
98
- end
62
+ def produces(*content_types)
63
+ metadata[:openapi_operation]&.produces(*content_types)
99
64
  end
100
65
 
101
- private
102
-
103
- def method_missing(name, ...)
104
- if @example_group.respond_to?(name)
105
- @example_group.send(name, ...)
106
- else
107
- super
108
- end
66
+ def request_body(attributes = {})
67
+ metadata[:openapi_operation]&.request_body(attributes)
109
68
  end
110
69
 
111
- def respond_to_missing?(name, include_private = false)
112
- @example_group.respond_to?(name, include_private) || super
70
+ def request_body_example(**kwargs)
71
+ metadata[:openapi_operation]&.request_body_example(**kwargs)
113
72
  end
114
- end
115
73
 
116
- class ResponseProxy
117
- def initialize(example_group, response_context)
118
- @example_group = example_group
119
- @response = response_context
74
+ def response(status_code, description, hidden: false, &block)
75
+ operation = metadata[:openapi_operation]
76
+ response_ctx = operation.response(status_code, description, hidden: hidden)
77
+
78
+ context "response #{status_code} #{description}" do
79
+ metadata[:openapi_response] = response_ctx
80
+ instance_eval(&block) if block
81
+ end
120
82
  end
121
83
 
122
84
  def schema(definition)
123
- @response.schema(definition)
85
+ metadata[:openapi_response]&.schema(definition)
124
86
  end
125
87
 
126
88
  def header(name, attributes = {})
127
- @response.header(name, attributes)
89
+ metadata[:openapi_response]&.header(name, attributes)
128
90
  end
129
91
 
130
- def example(content_type, **)
131
- @response.example(content_type, **)
132
- end
92
+ def run_test!(description = nil, &block)
93
+ response_ctx = metadata[:openapi_response]
133
94
 
134
- def produces(*content_types)
135
- @response.produces(*content_types)
95
+ before do |example|
96
+ submit_openapi_request(example.metadata)
97
+ end
98
+
99
+ it(description || "returns #{response_ctx.status_code}") do |example|
100
+ assert_openapi_response(example.metadata)
101
+ instance_eval(&block) if block
102
+ end
136
103
  end
104
+ end
137
105
 
138
- def run_test!(description = nil, &block)
139
- response_ctx = @response
140
- @example_group.it(description || "returns #{response_ctx.status_code}") do |example|
141
- example_metadata = example.metadata
142
-
143
- # Resolve path parameters from let variables
144
- path = resolve_path(example_metadata)
145
- operation = find_operation(example_metadata)
146
-
147
- # Build params and headers from let variables
148
- params = resolve_let(:request_params) || {}
149
- headers = resolve_let(:request_headers) || {}
150
- body = resolve_let(:request_body)
151
-
152
- # Merge individual parameter let values
153
- operation&.parameters&.each do |param|
154
- name = param["name"]
155
- val = resolve_let(name.to_sym)
156
- next unless val
157
-
158
- case param["in"]
159
- when "query"
160
- params[name] = val
161
- when "header"
162
- headers[name] = val
163
- when "path"
164
- # Already handled in resolve_path
165
- end
166
- end
106
+ # Instance-level helper methods mixed into RSpec examples
107
+ module ExampleHelpers
108
+ # submit_openapi_request is public so specs can call it directly
109
+ # (e.g., for rate limiting tests that need multiple requests)
110
+ def submit_openapi_request(metadata)
111
+ path = resolve_path(metadata)
112
+ operation = find_in_metadata(metadata, :openapi_operation)
113
+
114
+ params = resolve_let(:request_params) || {}
115
+ headers = resolve_let(:request_headers) || {}
116
+ body = resolve_let(:request_body)
117
+
118
+ # Merge individual parameter let values
119
+ operation&.parameters&.each do |param|
120
+ name = param["name"]
121
+ val = resolve_let(name.to_sym)
122
+ next if val.nil?
167
123
 
168
- # Execute the request
169
- method = operation&.verb || example_metadata[:openapi_operation]&.verb || "get"
170
- send_args = {params: body || params}
171
- send_args[:headers] = headers if headers.any?
172
-
173
- if body
174
- content_type = headers["Content-Type"] || operation&.instance_variable_get(:@consumes_list)&.first
175
-
176
- if content_type&.include?("form-data") || content_type&.include?("x-www-form-urlencoded")
177
- send_args[:params] = body
178
- send_args[:headers] = (headers || {}).merge("Content-Type" => content_type)
179
- else
180
- send_args[:params] = body.is_a?(String) ? body : body.to_json
181
- send_args[:headers] = (headers || {}).merge("Content-Type" => content_type || "application/json")
182
- end
124
+ case param["in"]
125
+ when "query" then params[name] = val
126
+ when "header" then headers[name] = val
183
127
  end
128
+ end
184
129
 
185
- send(method.to_sym, path, **send_args)
186
-
187
- # Validate response
188
- if OpenapiRuby.configuration.validate_responses_in_tests && response_ctx.schema_definition
189
- validator = Testing::ResponseValidator.new
190
- errors = validator.validate(
191
- response_body: parsed_response_body,
192
- status_code: response.status,
193
- response_context: response_ctx
194
- )
195
- expect(errors).to be_empty, "Response validation failed:\n#{errors.join("\n")}"
196
- else
197
- expect(response.status).to eq(response_ctx.status_code.to_i)
198
- end
130
+ # Resolve security scheme parameters from let variables
131
+ resolve_security_params(operation, metadata).each do |param|
132
+ val = resolve_let(param[:name].to_sym)
133
+ next unless val
199
134
 
200
- # Execute additional assertions
201
- instance_eval(&block) if block
135
+ case param[:in].to_s
136
+ when "header" then headers[param[:name]] = val
137
+ when "query" then params[param[:name]] = val
138
+ when "cookie" then headers["Cookie"] = "#{param[:name]}=#{val}"
139
+ end
202
140
  end
203
- end
204
141
 
205
- # Forward let/before/after/subject to RSpec
206
- def method_missing(name, ...)
207
- if @example_group.respond_to?(name)
208
- @example_group.send(name, ...)
142
+ method = operation&.verb || "get"
143
+ # Default to JSON Accept header for API requests
144
+ headers["Accept"] ||= "application/json"
145
+
146
+ if body
147
+ content_type = operation&.request_body_definition&.dig("content")&.keys&.first || "application/json"
148
+ request_args = if content_type.include?("form-data") || content_type.include?("x-www-form-urlencoded")
149
+ {params: body, headers: headers}
150
+ else
151
+ {
152
+ params: body.is_a?(String) ? body : body.to_json,
153
+ headers: headers.merge("Content-Type" => content_type)
154
+ }
155
+ end
156
+ # Append query params to path when body is present
157
+ if params.any?
158
+ query_string = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join("&")
159
+ path = "#{path}?#{query_string}"
160
+ end
209
161
  else
210
- super
162
+ request_args = {params: params, headers: headers}
211
163
  end
164
+
165
+ send(method.to_sym, path, **request_args)
212
166
  end
213
167
 
214
- def respond_to_missing?(name, include_private = false)
215
- @example_group.respond_to?(name, include_private) || super
168
+ def assert_openapi_response(metadata)
169
+ response_ctx = find_in_metadata(metadata, :openapi_response)
170
+
171
+ expected_status = response_ctx.status_code.to_i
172
+ actual_status = response.status
173
+
174
+ unless actual_status == expected_status
175
+ raise "Response validation failed:\n" \
176
+ "Expected status #{expected_status}, got #{actual_status}\n" \
177
+ "Response body: #{response.body}"
178
+ end
216
179
  end
217
- end
218
180
 
219
- # Helper methods mixed into RSpec examples
220
- module ExampleHelpers
221
181
  private
222
182
 
223
183
  def resolve_path(metadata)
224
- path_ctx = find_path_context(metadata)
184
+ path_ctx = find_in_metadata(metadata, :openapi_path_context)
225
185
  template = path_ctx&.path_template || ""
226
- find_operation(metadata)
227
186
 
228
- # Substitute {param} placeholders with let values
229
- template.gsub(/\{(\w+)\}/) do
187
+ base_path = resolve_base_path(path_ctx&.schema_name)
188
+ full_path = "#{base_path}#{template}"
189
+
190
+ full_path.gsub(/\{(\w+)\}/) do
230
191
  name = ::Regexp.last_match(1)
231
- val = resolve_let(name.to_sym)
232
- val || "{#{name}}"
192
+ resolve_let(name.to_sym) || "{#{name}}"
233
193
  end
234
194
  end
235
195
 
236
- def find_path_context(metadata)
196
+ def find_in_metadata(metadata, key)
237
197
  meta = metadata
238
198
  while meta
239
- return meta[:openapi_path_context] if meta[:openapi_path_context]
240
-
199
+ return meta[key] if meta[key]
241
200
  meta = meta[:parent_example_group]
242
201
  end
243
202
  nil
244
203
  end
245
204
 
246
- def find_operation(metadata)
247
- meta = metadata
248
- while meta
249
- return meta[:openapi_operation] if meta[:openapi_operation]
205
+ def resolve_base_path(schema_name)
206
+ return "" unless schema_name
250
207
 
251
- meta = meta[:parent_example_group]
208
+ config = OpenapiRuby.configuration
209
+ schema_config = config.schemas[schema_name.to_sym] || config.schemas[schema_name.to_s]
210
+ return "" unless schema_config
211
+
212
+ server_url = schema_config.dig(:servers, 0, :url) || schema_config.dig("servers", 0, "url")
213
+ return "" unless server_url
214
+
215
+ URI.parse(server_url).path.chomp("/")
216
+ rescue URI::InvalidURIError
217
+ ""
218
+ end
219
+
220
+ def resolve_security_params(operation, metadata)
221
+ security_list = operation&.instance_variable_get(:@security_list)
222
+ return [] unless security_list
223
+
224
+ schema_name = find_in_metadata(metadata, :openapi_schema_name)
225
+ return [] unless schema_name
226
+
227
+ config = OpenapiRuby.configuration
228
+ schema_config = config.schemas[schema_name.to_sym] || config.schemas[schema_name.to_s]
229
+ return [] unless schema_config
230
+
231
+ security_schemes = schema_config.dig(:components, :securitySchemes) ||
232
+ schema_config.dig("components", "securitySchemes") || {}
233
+
234
+ # Also check registered components
235
+ if security_schemes.empty?
236
+ loader = Components::Loader.new(scope: schema_name.to_s.tr("/", "_").to_sym)
237
+ security_schemes = loader.security_schemes
252
238
  end
253
- nil
239
+
240
+ scheme_names = security_list.flat_map { |s| s.is_a?(Hash) ? s.keys.map(&:to_s) : [] }
241
+
242
+ scheme_names.filter_map do |name|
243
+ scheme = security_schemes[name] || security_schemes[name.to_sym]
244
+ next unless scheme
245
+
246
+ type = scheme[:type] || scheme["type"]
247
+ if type.to_s == "apiKey"
248
+ {name: (scheme[:name] || scheme["name"]).to_s, in: (scheme[:in] || scheme["in"]).to_s}
249
+ else
250
+ # OAuth2, http bearer, etc. → Authorization header
251
+ {name: "Authorization", in: "header"}
252
+ end
253
+ end.uniq { |p| [p[:name], p[:in]] }
254
254
  end
255
255
 
256
256
  def resolve_let(name)
@@ -261,21 +261,10 @@ module OpenapiRuby
261
261
 
262
262
  def parsed_response_body
263
263
  return nil if response.body.empty?
264
-
265
264
  JSON.parse(response.body)
266
265
  rescue JSON::ParserError
267
266
  response.body
268
267
  end
269
-
270
- def operation_context_from_parent
271
- meta = self.class.metadata
272
- while meta
273
- return meta[:openapi_operation] if meta[:openapi_operation]
274
-
275
- meta = meta[:parent_example_group]
276
- end
277
- nil
278
- end
279
268
  end
280
269
 
281
270
  def self.install!
@@ -283,7 +272,6 @@ module OpenapiRuby
283
272
  config.extend ExampleGroupHelpers, type: :openapi
284
273
  config.include ExampleHelpers, type: :openapi
285
274
 
286
- # Make type: :openapi behave like request specs (includes integration test methods)
287
275
  if defined?(::RSpec::Rails)
288
276
  config.include ::RSpec::Rails::RequestExampleGroup, type: :openapi
289
277
  end
@@ -65,11 +65,11 @@ module OpenapiRuby
65
65
  existing_scopes = existing._component_scopes
66
66
  existing_scopes_set = existing._component_scopes_explicitly_set
67
67
 
68
- # Skip when exactly one side has explicitly configured scopes — during initial
68
+ # Skip when scopes haven't been explicitly configured yet — during initial
69
69
  # loading, components are registered with empty default scopes before the Loader
70
- # assigns inferred scopes. The unconfigured side may get scopes later via
71
- # component_scopes, which unregisters/re-registers and retriggers this check.
72
- next if new_scopes_set != existing_scopes_set
70
+ # assigns inferred scopes. Only check for duplicates when both sides have
71
+ # explicitly set their scopes.
72
+ next unless new_scopes_set && existing_scopes_set
73
73
 
74
74
  if scopes_overlap?(new_scopes, existing_scopes)
75
75
  raise DuplicateComponentError,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiRuby
4
- VERSION = "2.3.0"
4
+ VERSION = "2.4.0"
5
5
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :openapi_ruby do
4
- desc "Generate OpenAPI spec files from test definitions and components"
4
+ desc "Generate OpenAPI schema files from spec definitions and components"
5
5
  task generate: :environment do
6
- require "openapi_ruby"
7
- OpenapiRuby::Generator::SchemaWriter.generate_all!
6
+ pattern = ENV.fetch("PATTERN", "spec/**/*_spec.rb")
7
+ command = "bundle exec rspec --pattern '#{pattern}' --dry-run --order defined"
8
+ puts "Generating OpenAPI schemas..."
9
+ system(command) || abort("Schema generation failed")
8
10
  end
9
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Hartvig