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 +4 -4
- data/lib/openapi_ruby/adapters/minitest.rb +97 -32
- data/lib/openapi_ruby/adapters/rspec.rb +172 -184
- data/lib/openapi_ruby/components/registry.rb +4 -4
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/tasks/openapi_ruby.rake +5 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 625d042fe294eeec35636e245418a38ab1cdefb13412fb1cfd355429a90ead83
|
|
4
|
+
data.tar.gz: 354e09c385f606421e191f05bccfef6a1cefac42be17485f3eeb4bf38ccf3285
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
70
|
+
def request_body_example(**kwargs)
|
|
71
|
+
metadata[:openapi_operation]&.request_body_example(**kwargs)
|
|
113
72
|
end
|
|
114
|
-
end
|
|
115
73
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
85
|
+
metadata[:openapi_response]&.schema(definition)
|
|
124
86
|
end
|
|
125
87
|
|
|
126
88
|
def header(name, attributes = {})
|
|
127
|
-
|
|
89
|
+
metadata[:openapi_response]&.header(name, attributes)
|
|
128
90
|
end
|
|
129
91
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
end
|
|
92
|
+
def run_test!(description = nil, &block)
|
|
93
|
+
response_ctx = metadata[:openapi_response]
|
|
133
94
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
215
|
-
|
|
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 =
|
|
184
|
+
path_ctx = find_in_metadata(metadata, :openapi_path_context)
|
|
225
185
|
template = path_ctx&.path_template || ""
|
|
226
|
-
find_operation(metadata)
|
|
227
186
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
val || "{#{name}}"
|
|
192
|
+
resolve_let(name.to_sym) || "{#{name}}"
|
|
233
193
|
end
|
|
234
194
|
end
|
|
235
195
|
|
|
236
|
-
def
|
|
196
|
+
def find_in_metadata(metadata, key)
|
|
237
197
|
meta = metadata
|
|
238
198
|
while meta
|
|
239
|
-
return meta[
|
|
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
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
71
|
-
#
|
|
72
|
-
next
|
|
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,
|
data/lib/openapi_ruby/version.rb
CHANGED
data/lib/tasks/openapi_ruby.rake
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
namespace :openapi_ruby do
|
|
4
|
-
desc "Generate OpenAPI
|
|
4
|
+
desc "Generate OpenAPI schema files from spec definitions and components"
|
|
5
5
|
task generate: :environment do
|
|
6
|
-
|
|
7
|
-
|
|
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
|