openapi-ruby 3.1.1 → 3.2.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/README.md +74 -2
- data/lib/openapi_ruby/adapters/minitest.rb +32 -0
- data/lib/openapi_ruby/adapters/rspec.rb +162 -0
- data/lib/openapi_ruby/configuration.rb +5 -0
- data/lib/openapi_ruby/testing/request_validator.rb +153 -0
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/openapi_ruby.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e97365e4c2b566d59db1f4946a2b0e1f45f8ee9b938f2b5c049ac0ac9f56dcc
|
|
4
|
+
data.tar.gz: 431709a674d6a028fd8a14f47200761215a6e4576888009b3b2b637fd5a453b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a670b21d1043c474b2eead1844c47258521df05b247d561a5578de4c8495a6519b575c7b3d2d4fe4fc54c22d5d524e45ee2fab492e99e3366c8eab5bc381ecf
|
|
7
|
+
data.tar.gz: e2981bfa965fef6c6db6767d8e36592b8166fa20f675265c9d0147265565250e845093ca4fb3dc3bd3a7d2a2b1c3a7c6cb9d52169a0142bc0be2a90727a50b77
|
data/README.md
CHANGED
|
@@ -68,6 +68,10 @@ OpenapiRuby.configure do |config|
|
|
|
68
68
|
config.request_validation = :disabled # :enabled, :disabled, :warn_only
|
|
69
69
|
config.response_validation = :disabled
|
|
70
70
|
|
|
71
|
+
# Test DSL: validate requests against declared operations before sending.
|
|
72
|
+
# Enabled by default; set to false to disable.
|
|
73
|
+
config.test_request_validation = true
|
|
74
|
+
|
|
71
75
|
# Optional Swagger UI (disabled by default)
|
|
72
76
|
config.ui_enabled = false
|
|
73
77
|
end
|
|
@@ -230,6 +234,12 @@ rails generate openapi_ruby:component BearerAuth security_schemes
|
|
|
230
234
|
require "openapi_ruby/rspec"
|
|
231
235
|
```
|
|
232
236
|
|
|
237
|
+
RSpec supports two DSL styles. Both generate the same OpenAPI spec and validate responses (and requests) against it.
|
|
238
|
+
|
|
239
|
+
### Style 1: `path` / `run_test!`
|
|
240
|
+
|
|
241
|
+
Schema definition and test execution are interleaved. Each `response` block uses `let` values and `run_test!` to send the request inline:
|
|
242
|
+
|
|
233
243
|
```ruby
|
|
234
244
|
# spec/requests/users_spec.rb
|
|
235
245
|
require "openapi_helper"
|
|
@@ -293,11 +303,71 @@ RSpec.describe "Users API", type: :openapi do
|
|
|
293
303
|
end
|
|
294
304
|
```
|
|
295
305
|
|
|
306
|
+
### Style 2: `api_path` / `assert_api_response`
|
|
307
|
+
|
|
308
|
+
Schema definition at the top, normal RSpec examples underneath. Mirrors the Minitest DSL and is useful when you want basic schema validation separated from detailed edge-case tests:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
require "openapi_helper"
|
|
312
|
+
|
|
313
|
+
RSpec.describe "Users API", type: :openapi do
|
|
314
|
+
openapi_schema :public_api
|
|
315
|
+
|
|
316
|
+
api_path "/api/v1/users" do
|
|
317
|
+
get "List users" do
|
|
318
|
+
tags "Users"
|
|
319
|
+
produces "application/json"
|
|
320
|
+
|
|
321
|
+
response 200, "returns all users" do
|
|
322
|
+
schema type: :array, items: { "$ref" => "#/components/schemas/User" }
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
post "Create a user" do
|
|
327
|
+
consumes "application/json"
|
|
328
|
+
|
|
329
|
+
request_body required: true, content: {
|
|
330
|
+
"application/json" => {
|
|
331
|
+
schema: { "$ref" => "#/components/schemas/UserInput" }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
response 201, "user created" do
|
|
336
|
+
schema "$ref" => "#/components/schemas/User"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
response 422, "validation errors" do
|
|
340
|
+
schema "$ref" => "#/components/schemas/ValidationErrors"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Normal RSpec examples
|
|
346
|
+
it "returns all users" do
|
|
347
|
+
User.create!(name: "Jane", email: "jane@example.com")
|
|
348
|
+
|
|
349
|
+
assert_api_response :get, 200 do
|
|
350
|
+
expect(parsed_body.length).to eq(1)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it "creates a user" do
|
|
355
|
+
assert_api_response :post, 201, body: { name: "Jane", email: "jane@example.com" } do
|
|
356
|
+
expect(parsed_body["name"]).to eq("Jane")
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`assert_api_response` accepts `params:`, `headers:`, `body:`, and `path_params:` keyword arguments. It validates the response status and body schema automatically, then yields to the block for additional expectations.
|
|
363
|
+
|
|
296
364
|
### DSL Reference
|
|
297
365
|
|
|
298
366
|
| Method | Level | Description |
|
|
299
367
|
|--------|-------|-------------|
|
|
300
|
-
| `path(template, &block)` | Top | Define an API path |
|
|
368
|
+
| `path(template, &block)` | Top | Define an API path (style 1) |
|
|
369
|
+
| `api_path(template, &block)` | Top | Define an API path (style 2) |
|
|
370
|
+
| `openapi_schema(name)` | Top | Set the schema name (style 2) |
|
|
301
371
|
| `get/post/put/patch/delete(summary, &block)` | Path | Define an operation |
|
|
302
372
|
| `tags(*tags)` | Operation | Tag the operation |
|
|
303
373
|
| `operationId(id)` | Operation | Set operation ID |
|
|
@@ -311,7 +381,9 @@ end
|
|
|
311
381
|
| `response(status, description, &block)` | Operation | Define expected response |
|
|
312
382
|
| `schema(definition)` | Response | Response body schema |
|
|
313
383
|
| `header(name, schema:, **opts)` | Response | Response header |
|
|
314
|
-
| `run_test!(&block)` | Response | Execute request and validate |
|
|
384
|
+
| `run_test!(&block)` | Response | Execute request and validate (style 1) |
|
|
385
|
+
| `assert_api_response(method, status, **opts, &block)` | Example | Execute request and validate (style 2) |
|
|
386
|
+
| `parsed_body` | Example | Parsed JSON response body |
|
|
315
387
|
|
|
316
388
|
## Testing with Minitest
|
|
317
389
|
|
|
@@ -81,6 +81,21 @@ module OpenapiRuby
|
|
|
81
81
|
request_args = {params: query_params, headers: headers}
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Validate the request against the declared operation (skip for error responses,
|
|
85
|
+
# since those tests intentionally send invalid data)
|
|
86
|
+
if OpenapiRuby.configuration.test_request_validation && expected_status < 400
|
|
87
|
+
document_hash = build_validation_document(context.schema_name)
|
|
88
|
+
req_errors = Testing::RequestValidator.new(document_hash).validate(
|
|
89
|
+
operation: operation,
|
|
90
|
+
path_context: context,
|
|
91
|
+
params: params,
|
|
92
|
+
headers: headers,
|
|
93
|
+
body: body,
|
|
94
|
+
path_params: path_params
|
|
95
|
+
)
|
|
96
|
+
assert req_errors.empty?, "Request validation failed:\n#{req_errors.join("\n")}"
|
|
97
|
+
end
|
|
98
|
+
|
|
84
99
|
send(method, path, **request_args)
|
|
85
100
|
|
|
86
101
|
# Validate response
|
|
@@ -181,6 +196,23 @@ module OpenapiRuby
|
|
|
181
196
|
end.uniq { |p| [p[:name], p[:in]] }
|
|
182
197
|
end
|
|
183
198
|
|
|
199
|
+
def build_validation_document(schema_name)
|
|
200
|
+
return nil unless schema_name
|
|
201
|
+
|
|
202
|
+
config = OpenapiRuby.configuration
|
|
203
|
+
schema_config = config.schemas[schema_name.to_sym] || config.schemas[schema_name.to_s]
|
|
204
|
+
return nil unless schema_config
|
|
205
|
+
|
|
206
|
+
builder = OpenapiRuby::Core::DocumentBuilder.new(schema_config)
|
|
207
|
+
OpenapiRuby::DSL::MetadataStore.contexts_for(schema_name).each do |ctx|
|
|
208
|
+
builder.add_path(ctx.path_template, ctx.to_openapi)
|
|
209
|
+
end
|
|
210
|
+
scope = schema_config[:component_scope]
|
|
211
|
+
loader = OpenapiRuby::Components::Loader.new(scope: scope)
|
|
212
|
+
builder.merge_components(loader.to_openapi_hash)
|
|
213
|
+
builder.build.data
|
|
214
|
+
end
|
|
215
|
+
|
|
184
216
|
def parse_response_body
|
|
185
217
|
return nil if response.body.empty?
|
|
186
218
|
|
|
@@ -11,6 +11,22 @@ module OpenapiRuby
|
|
|
11
11
|
# All methods are inherited by nested describe/context/it_behaves_like blocks.
|
|
12
12
|
# Data is stored in RSpec metadata which propagates to child groups.
|
|
13
13
|
module ExampleGroupHelpers
|
|
14
|
+
def openapi_schema(name)
|
|
15
|
+
metadata[:openapi_schema_name] = name.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Minitest-style DSL: define the schema at the top of the spec file,
|
|
19
|
+
# then write normal RSpec examples underneath using assert_api_response.
|
|
20
|
+
def api_path(template, &block)
|
|
21
|
+
schema_name = metadata[:openapi_schema_name]
|
|
22
|
+
context = DSL::Context.new(template, schema_name: schema_name)
|
|
23
|
+
context.instance_eval(&block) if block
|
|
24
|
+
metadata[:openapi_api_contexts] ||= []
|
|
25
|
+
metadata[:openapi_api_contexts] << context
|
|
26
|
+
DSL::MetadataStore.register(context)
|
|
27
|
+
context
|
|
28
|
+
end
|
|
29
|
+
|
|
14
30
|
def path(template, &block)
|
|
15
31
|
schema_name = metadata[:openapi_schema_name]
|
|
16
32
|
context = DSL::Context.new(template, schema_name: schema_name)
|
|
@@ -104,6 +120,100 @@ module OpenapiRuby
|
|
|
104
120
|
|
|
105
121
|
# Instance-level helper methods mixed into RSpec examples
|
|
106
122
|
module ExampleHelpers
|
|
123
|
+
# Minitest-style assertion: looks up the api_path context, makes the
|
|
124
|
+
# request, validates the response status + body, then yields to the
|
|
125
|
+
# block for additional expectations.
|
|
126
|
+
def assert_api_response(method, expected_status, params: {}, headers: {}, body: nil, path_params: {}, &block)
|
|
127
|
+
meta = ::RSpec.current_example.metadata
|
|
128
|
+
context = find_api_context_for(meta, method, path_params)
|
|
129
|
+
raise OpenapiRuby::Error, "No api_path defined for #{method.upcase} in this example group" unless context
|
|
130
|
+
|
|
131
|
+
operation = context.operations[method.to_s]
|
|
132
|
+
raise OpenapiRuby::Error, "No #{method.upcase} operation defined" unless operation
|
|
133
|
+
|
|
134
|
+
response_ctx = operation.responses[expected_status.to_s]
|
|
135
|
+
raise OpenapiRuby::Error, "No response #{expected_status} defined for #{method.upcase}" unless response_ctx
|
|
136
|
+
|
|
137
|
+
base_path = resolve_base_path(context.schema_name)
|
|
138
|
+
path = "#{base_path}#{expand_path(context.path_template, params.merge(path_params))}"
|
|
139
|
+
|
|
140
|
+
# Resolve security scheme parameters
|
|
141
|
+
resolve_security_params(operation, meta).each do |param|
|
|
142
|
+
val = params[param[:name].to_sym] || params[param[:name]]
|
|
143
|
+
next if val.nil?
|
|
144
|
+
|
|
145
|
+
case param[:in].to_s
|
|
146
|
+
when "header" then headers[param[:name]] = val
|
|
147
|
+
when "query" then params[param[:name]] = val
|
|
148
|
+
when "cookie" then headers["Cookie"] = "#{param[:name]}=#{val}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
headers["Accept"] ||= "application/json"
|
|
153
|
+
|
|
154
|
+
path_param_names = context.path_parameters.map { |p| p["name"] }
|
|
155
|
+
query_params = params.reject { |k, _| path_param_names.include?(k.to_s) }
|
|
156
|
+
|
|
157
|
+
if body
|
|
158
|
+
content_type = operation.request_body_definition&.dig("content")&.keys&.first || "application/json"
|
|
159
|
+
request_args = if content_type.include?("form-data") || content_type.include?("x-www-form-urlencoded")
|
|
160
|
+
{params: body, headers: headers}
|
|
161
|
+
else
|
|
162
|
+
{
|
|
163
|
+
params: body.is_a?(String) ? body : body.to_json,
|
|
164
|
+
headers: headers.merge("Content-Type" => content_type)
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
request_args = {headers: headers}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if query_params.any?
|
|
172
|
+
path = "#{path}?#{Rack::Utils.build_nested_query(query_params)}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Validate the request against the declared operation (skip for error responses,
|
|
176
|
+
# since those tests intentionally send invalid data)
|
|
177
|
+
if OpenapiRuby.configuration.test_request_validation && expected_status < 400
|
|
178
|
+
document_hash = OpenapiRuby::Adapters::RSpec.validation_document_for(context.schema_name)
|
|
179
|
+
req_errors = Testing::RequestValidator.new(document_hash).validate(
|
|
180
|
+
operation: operation,
|
|
181
|
+
path_context: context,
|
|
182
|
+
params: params,
|
|
183
|
+
headers: headers,
|
|
184
|
+
body: body,
|
|
185
|
+
path_params: path_params
|
|
186
|
+
)
|
|
187
|
+
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
send(method, path, **request_args)
|
|
191
|
+
|
|
192
|
+
unless response.status == expected_status
|
|
193
|
+
raise "Expected status #{expected_status}, got #{response.status}\nResponse body: #{response.body}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if response_ctx.schema_definition
|
|
197
|
+
validator = Testing::ResponseValidator.new(
|
|
198
|
+
OpenapiRuby::Adapters::RSpec.validation_document_for(context.schema_name)
|
|
199
|
+
)
|
|
200
|
+
errors = validator.validate(
|
|
201
|
+
response_body: parsed_response_body,
|
|
202
|
+
status_code: response.status,
|
|
203
|
+
response_context: response_ctx
|
|
204
|
+
)
|
|
205
|
+
unless errors.empty?
|
|
206
|
+
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{response.body}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
instance_eval(&block) if block
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parsed_body
|
|
214
|
+
parsed_response_body
|
|
215
|
+
end
|
|
216
|
+
|
|
107
217
|
# submit_openapi_request is public so specs can call it directly
|
|
108
218
|
# (e.g., for rate limiting tests that need multiple requests)
|
|
109
219
|
def submit_openapi_request(metadata)
|
|
@@ -164,6 +274,35 @@ module OpenapiRuby
|
|
|
164
274
|
request_args = {headers: headers}
|
|
165
275
|
end
|
|
166
276
|
|
|
277
|
+
# Validate the request against the declared operation (skip for error responses,
|
|
278
|
+
# since those tests intentionally send invalid data)
|
|
279
|
+
response_ctx = find_in_metadata(metadata, :openapi_response)
|
|
280
|
+
expected_status = response_ctx&.status_code.to_i
|
|
281
|
+
if OpenapiRuby.configuration.test_request_validation && operation && expected_status < 400
|
|
282
|
+
path_ctx = find_in_metadata(metadata, :openapi_path_context)
|
|
283
|
+
schema_name = find_in_metadata(metadata, :openapi_schema_name)
|
|
284
|
+
|
|
285
|
+
# Collect resolved path param values for validation
|
|
286
|
+
path_param_values = {}
|
|
287
|
+
(path_ctx&.path_parameters || []).each do |param|
|
|
288
|
+
name = param["name"]
|
|
289
|
+
next unless name
|
|
290
|
+
val = resolve_let(name.to_sym)
|
|
291
|
+
path_param_values[name] = val if val
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
document_hash = OpenapiRuby::Adapters::RSpec.validation_document_for(schema_name)
|
|
295
|
+
req_errors = Testing::RequestValidator.new(document_hash).validate(
|
|
296
|
+
operation: operation,
|
|
297
|
+
path_context: path_ctx,
|
|
298
|
+
params: params,
|
|
299
|
+
headers: headers,
|
|
300
|
+
body: body,
|
|
301
|
+
path_params: path_param_values
|
|
302
|
+
)
|
|
303
|
+
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
304
|
+
end
|
|
305
|
+
|
|
167
306
|
send(method.to_sym, path, **request_args)
|
|
168
307
|
end
|
|
169
308
|
|
|
@@ -195,6 +334,29 @@ module OpenapiRuby
|
|
|
195
334
|
|
|
196
335
|
private
|
|
197
336
|
|
|
337
|
+
def find_api_context_for(metadata, method, path_params)
|
|
338
|
+
contexts = find_in_metadata(metadata, :openapi_api_contexts) || []
|
|
339
|
+
has_path_params = path_params.any?
|
|
340
|
+
|
|
341
|
+
contexts.find do |ctx|
|
|
342
|
+
next false unless ctx.operations.key?(method.to_s)
|
|
343
|
+
|
|
344
|
+
if has_path_params
|
|
345
|
+
ctx.path_template.include?("{")
|
|
346
|
+
else
|
|
347
|
+
!ctx.path_template.include?("{")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def expand_path(template, params)
|
|
353
|
+
template.gsub(/\{(\w+)\}/) do
|
|
354
|
+
name = ::Regexp.last_match(1)
|
|
355
|
+
value = params[name.to_sym] || params[name.to_s]
|
|
356
|
+
value || "{#{name}}"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
198
360
|
def resolve_path(metadata)
|
|
199
361
|
path_ctx = find_in_metadata(metadata, :openapi_path_context)
|
|
200
362
|
template = path_ctx&.path_template || ""
|
|
@@ -18,6 +18,10 @@ module OpenapiRuby
|
|
|
18
18
|
# Middleware (runtime validation)
|
|
19
19
|
attr_accessor :request_validation, :response_validation, :coerce_params
|
|
20
20
|
|
|
21
|
+
# Test DSL: validate that requests match the declared operation before sending.
|
|
22
|
+
# Enabled by default; set to false to disable.
|
|
23
|
+
attr_accessor :test_request_validation
|
|
24
|
+
|
|
21
25
|
# OpenAPI meta-schema validation of generated specs and middleware-loaded
|
|
22
26
|
# documents. One of :disabled, :enabled (raise on errors), :warn_only
|
|
23
27
|
# (default, log warnings). Boolean values are accepted for backwards
|
|
@@ -46,6 +50,7 @@ module OpenapiRuby
|
|
|
46
50
|
@request_validation = :disabled
|
|
47
51
|
@response_validation = :disabled
|
|
48
52
|
@coerce_params = true
|
|
53
|
+
@test_request_validation = true
|
|
49
54
|
@schema_output_dir = "swagger"
|
|
50
55
|
@schema_output_format = :yaml
|
|
51
56
|
@ui_enabled = false
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiRuby
|
|
4
|
+
module Testing
|
|
5
|
+
class RequestValidator
|
|
6
|
+
def initialize(document_hash = nil)
|
|
7
|
+
@document_hash = document_hash
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate(operation:, path_context: nil, params: {}, headers: {}, body: nil, path_params: {})
|
|
11
|
+
errors = []
|
|
12
|
+
|
|
13
|
+
# Collect all parameters (path-level + operation-level)
|
|
14
|
+
all_parameters = (path_context&.path_parameters || []) + (operation.parameters || [])
|
|
15
|
+
|
|
16
|
+
# Validate each declared parameter
|
|
17
|
+
all_parameters.each do |param|
|
|
18
|
+
name = param["name"]
|
|
19
|
+
next unless name
|
|
20
|
+
|
|
21
|
+
value = extract_param_value(param, params, headers, path_params)
|
|
22
|
+
|
|
23
|
+
if value.nil?
|
|
24
|
+
errors << "Missing required #{param["in"]} parameter: #{name}" if param["required"]
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if param["schema"]
|
|
29
|
+
param_errors = validate_value(value, param["schema"], "#{param["in"]} parameter '#{name}'")
|
|
30
|
+
errors.concat(param_errors)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Validate request body
|
|
35
|
+
errors.concat(validate_request_body(operation, body))
|
|
36
|
+
|
|
37
|
+
errors
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def extract_param_value(param, params, headers, path_params)
|
|
43
|
+
name = param["name"]
|
|
44
|
+
|
|
45
|
+
case param["in"]
|
|
46
|
+
when "query"
|
|
47
|
+
params[name.to_sym] || params[name.to_s]
|
|
48
|
+
when "path"
|
|
49
|
+
path_params[name.to_sym] || path_params[name.to_s]
|
|
50
|
+
when "header"
|
|
51
|
+
headers[name] || headers[name.downcase]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_request_body(operation, body)
|
|
56
|
+
errors = []
|
|
57
|
+
rb_spec = operation.request_body_definition
|
|
58
|
+
return errors unless rb_spec
|
|
59
|
+
|
|
60
|
+
if rb_spec["required"] && body.nil?
|
|
61
|
+
errors << "Request body is required"
|
|
62
|
+
return errors
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return errors unless body && rb_spec["content"]
|
|
66
|
+
|
|
67
|
+
media_type = rb_spec["content"].keys.first
|
|
68
|
+
schema = rb_spec.dig("content", media_type, "schema")
|
|
69
|
+
return errors unless schema
|
|
70
|
+
|
|
71
|
+
validate_against_schema(body, schema, "request body", errors)
|
|
72
|
+
errors
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_value(value, schema, context)
|
|
76
|
+
coerced = coerce_for_validation(value, schema)
|
|
77
|
+
schema_validator = resolve_schema(schema)
|
|
78
|
+
return [] unless schema_validator
|
|
79
|
+
|
|
80
|
+
schema_validator.validate(coerced).map do |err|
|
|
81
|
+
"Invalid #{context}: #{format_error(err)}"
|
|
82
|
+
end
|
|
83
|
+
rescue => e
|
|
84
|
+
["Invalid #{context}: #{e.message}"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_against_schema(data, schema, context, errors)
|
|
88
|
+
schema_validator = resolve_schema(schema)
|
|
89
|
+
return unless schema_validator
|
|
90
|
+
|
|
91
|
+
schema_validator.validate(data).each do |err|
|
|
92
|
+
pointer = err["data_pointer"] || ""
|
|
93
|
+
msg = format_error(err)
|
|
94
|
+
location = pointer.empty? ? context : "#{context} at #{pointer}"
|
|
95
|
+
errors << "Invalid #{location}: #{msg}"
|
|
96
|
+
end
|
|
97
|
+
rescue => e
|
|
98
|
+
errors << "Invalid #{context}: #{e.message}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_schema(schema)
|
|
102
|
+
if schema.is_a?(Hash) && schema["$ref"] && @document_hash
|
|
103
|
+
JSONSchemer.openapi(@document_hash).ref(schema["$ref"])
|
|
104
|
+
elsif contains_ref?(schema)
|
|
105
|
+
nil # Cannot validate $ref without document
|
|
106
|
+
else
|
|
107
|
+
JSONSchemer.schema(schema)
|
|
108
|
+
end
|
|
109
|
+
rescue
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def coerce_for_validation(value, schema)
|
|
114
|
+
return value unless value.is_a?(String)
|
|
115
|
+
|
|
116
|
+
type = schema.is_a?(Hash) ? schema["type"] : nil
|
|
117
|
+
case type
|
|
118
|
+
when "integer" then Integer(value)
|
|
119
|
+
when "number" then Float(value)
|
|
120
|
+
when "boolean"
|
|
121
|
+
case value.downcase
|
|
122
|
+
when "true", "1" then true
|
|
123
|
+
when "false", "0" then false
|
|
124
|
+
else value
|
|
125
|
+
end
|
|
126
|
+
else value
|
|
127
|
+
end
|
|
128
|
+
rescue ArgumentError, TypeError
|
|
129
|
+
value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def contains_ref?(value)
|
|
133
|
+
case value
|
|
134
|
+
when Hash
|
|
135
|
+
return true if value.key?("$ref")
|
|
136
|
+
value.values.any? { |v| contains_ref?(v) }
|
|
137
|
+
when Array
|
|
138
|
+
value.any? { |v| contains_ref?(v) }
|
|
139
|
+
else
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def format_error(error)
|
|
145
|
+
if error.is_a?(Hash)
|
|
146
|
+
error["error"] || error["type"] || "validation failed"
|
|
147
|
+
else
|
|
148
|
+
error.to_s
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/openapi_ruby/version.rb
CHANGED
data/lib/openapi_ruby.rb
CHANGED
|
@@ -43,6 +43,7 @@ require_relative "openapi_ruby/dsl/context"
|
|
|
43
43
|
require_relative "openapi_ruby/dsl/metadata_store"
|
|
44
44
|
require_relative "openapi_ruby/testing/request_builder"
|
|
45
45
|
require_relative "openapi_ruby/testing/response_validator"
|
|
46
|
+
require_relative "openapi_ruby/testing/request_validator"
|
|
46
47
|
require_relative "openapi_ruby/testing/assertions"
|
|
47
48
|
require_relative "openapi_ruby/testing/coverage"
|
|
48
49
|
require_relative "openapi_ruby/generator/schema_writer"
|
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: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Morten Hartvig
|
|
@@ -117,6 +117,7 @@ files:
|
|
|
117
117
|
- lib/openapi_ruby/testing/assertions.rb
|
|
118
118
|
- lib/openapi_ruby/testing/coverage.rb
|
|
119
119
|
- lib/openapi_ruby/testing/request_builder.rb
|
|
120
|
+
- lib/openapi_ruby/testing/request_validator.rb
|
|
120
121
|
- lib/openapi_ruby/testing/response_validator.rb
|
|
121
122
|
- lib/openapi_ruby/version.rb
|
|
122
123
|
- lib/tasks/openapi_ruby.rake
|