committee 5.6.1 → 5.6.3
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/committee/drivers/open_api_3/driver.rb +7 -1
- data/lib/committee/drivers.rb +2 -3
- data/lib/committee/errors.rb +11 -0
- data/lib/committee/middleware/base.rb +11 -5
- data/lib/committee/middleware/options/base.rb +107 -0
- data/lib/committee/middleware/options/request_validation.rb +80 -0
- data/lib/committee/middleware/options/response_validation.rb +46 -0
- data/lib/committee/middleware/options.rb +12 -0
- data/lib/committee/middleware/request_validation.rb +7 -1
- data/lib/committee/middleware/response_validation.rb +6 -2
- data/lib/committee/middleware.rb +1 -0
- data/lib/committee/schema_validator/hyper_schema/response_generator.rb +1 -3
- data/lib/committee/schema_validator/hyper_schema/response_validator.rb +14 -1
- data/lib/committee/schema_validator/hyper_schema/router.rb +1 -1
- data/lib/committee/schema_validator/hyper_schema.rb +3 -14
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +43 -13
- data/lib/committee/schema_validator/open_api_3/parameter_deserializer.rb +556 -0
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +16 -2
- data/lib/committee/schema_validator/open_api_3.rb +19 -13
- data/lib/committee/schema_validator/option.rb +6 -17
- data/lib/committee/schema_validator.rb +12 -1
- data/lib/committee/test/except_parameter.rb +416 -0
- data/lib/committee/test/methods.rb +38 -2
- data/lib/committee/version.rb +1 -1
- data/lib/committee.rb +1 -1
- data/test/drivers/open_api_2/driver_test.rb +4 -16
- data/test/drivers/open_api_2/parameter_schema_builder_test.rb +4 -50
- data/test/drivers_test.rb +35 -21
- data/test/middleware/options/base_test.rb +120 -0
- data/test/middleware/options/request_validation_test.rb +177 -0
- data/test/middleware/options/response_validation_test.rb +121 -0
- data/test/middleware/request_validation_open_api_3_test.rb +200 -80
- data/test/middleware/request_validation_test.rb +13 -70
- data/test/middleware/response_validation_open_api_3_test.rb +40 -17
- data/test/middleware/response_validation_test.rb +3 -14
- data/test/request_unpacker_test.rb +2 -10
- data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +1 -37
- data/test/schema_validator/hyper_schema/request_validator_test.rb +6 -30
- data/test/schema_validator/hyper_schema/router_test.rb +5 -0
- data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +1 -37
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +58 -43
- data/test/schema_validator/open_api_3/parameter_deserializer_test.rb +457 -0
- data/test/schema_validator/open_api_3/request_validator_test.rb +1 -2
- data/test/schema_validator/open_api_3/response_validator_test.rb +3 -11
- data/test/schema_validator_test.rb +41 -0
- data/test/test/methods_test.rb +238 -105
- data/test/test/schema_coverage_test.rb +8 -155
- metadata +11 -1
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Committee
|
|
6
|
+
module SchemaValidator
|
|
7
|
+
class OpenAPI3
|
|
8
|
+
# Deserializes request parameters based on OpenAPI 3 parameter style and explode settings
|
|
9
|
+
#
|
|
10
|
+
# @see https://swagger.io/docs/specification/serialization/
|
|
11
|
+
# @see https://spec.openapis.org/oas/latest.html#parameter-object
|
|
12
|
+
class ParameterDeserializer
|
|
13
|
+
# @param [OpenAPIParser::RequestOperation] request_operation
|
|
14
|
+
def initialize(request_operation)
|
|
15
|
+
@request_operation = request_operation
|
|
16
|
+
@parameters = request_operation.operation_object.parameters || []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Deserialize query parameters
|
|
20
|
+
# @param [Hash] raw_params Raw query parameters from Rack
|
|
21
|
+
# @return [Hash] Deserialized parameters according to OpenAPI schema
|
|
22
|
+
def deserialize_query_params(raw_params)
|
|
23
|
+
deserialize_params_by_location(raw_params, 'query')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Deserialize path parameters
|
|
27
|
+
# @param [Hash] raw_params Raw path parameters
|
|
28
|
+
# @return [Hash] Deserialized parameters according to OpenAPI schema
|
|
29
|
+
def deserialize_path_params(raw_params)
|
|
30
|
+
deserialize_params_by_location(raw_params, 'path')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Deserialize header parameters
|
|
34
|
+
# @param [Hash] raw_headers Raw headers
|
|
35
|
+
# @return [Hash] Deserialized headers according to OpenAPI schema
|
|
36
|
+
def deserialize_headers(raw_headers)
|
|
37
|
+
deserialize_params_by_location(raw_headers, 'header')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Deserialize parameters for a specific location (query, path, header)
|
|
43
|
+
# @param [Hash] raw_params Raw parameters
|
|
44
|
+
# @param [String] location Parameter location ('query', 'path', 'header')
|
|
45
|
+
# @return [Hash] Deserialized parameters
|
|
46
|
+
def deserialize_params_by_location(raw_params, location)
|
|
47
|
+
return raw_params if raw_params.nil? || raw_params.empty?
|
|
48
|
+
|
|
49
|
+
result = Committee::Utils.indifferent_hash
|
|
50
|
+
params_for_location = @parameters.select { |p| p.in == location }
|
|
51
|
+
|
|
52
|
+
# If no parameters are defined for this location, return raw params as-is
|
|
53
|
+
return raw_params if params_for_location.empty?
|
|
54
|
+
|
|
55
|
+
raw_params = normalize_raw_params(raw_params, location, params_for_location)
|
|
56
|
+
|
|
57
|
+
# Collect parameter names that will be deserialized
|
|
58
|
+
# This includes both the parameter name and any properties (for exploded objects)
|
|
59
|
+
deserialized_keys = Set.new
|
|
60
|
+
|
|
61
|
+
# Deserialize each parameter defined in the schema
|
|
62
|
+
params_for_location.each do |param_def|
|
|
63
|
+
value = extract_and_deserialize(param_def, raw_params)
|
|
64
|
+
|
|
65
|
+
# Only include non-nil values
|
|
66
|
+
if !value.nil?
|
|
67
|
+
# For objects, ensure the result is also an indifferent hash
|
|
68
|
+
value = convert_to_indifferent_hash(value) if value.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
result[param_def.name] = value
|
|
71
|
+
deserialized_keys.add(param_def.name)
|
|
72
|
+
|
|
73
|
+
# For form-style exploded objects, mark the property keys as deserialized
|
|
74
|
+
if param_def.schema&.type == 'object' &&
|
|
75
|
+
(param_def.style.nil? || param_def.style == 'form') &&
|
|
76
|
+
(param_def.explode.nil? || param_def.explode)
|
|
77
|
+
param_def.schema.properties&.each_key do |prop_name|
|
|
78
|
+
deserialized_keys.add(prop_name.to_s)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# For deep object style, mark the bracket-notation keys as deserialized
|
|
83
|
+
if param_def.style == 'deepObject'
|
|
84
|
+
prefix = "#{param_def.name}["
|
|
85
|
+
raw_params.each_key do |key|
|
|
86
|
+
key_str = key.to_s
|
|
87
|
+
deserialized_keys.add(key) if key_str.start_with?(prefix)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Include params not in schema (for additionalProperties and unknown params)
|
|
94
|
+
# Only include params that weren't consumed by deserialization
|
|
95
|
+
raw_params.each do |key, value|
|
|
96
|
+
result[key] = value unless deserialized_keys.include?(key)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
result
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Convert a hash to an indifferent hash (supports both string and symbol keys)
|
|
103
|
+
# @param [Hash] hash
|
|
104
|
+
# @return [Committee::Utils::IndifferentHash]
|
|
105
|
+
def convert_to_indifferent_hash(hash)
|
|
106
|
+
return hash unless hash.is_a?(Hash)
|
|
107
|
+
Committee::Utils.indifferent_hash.merge(hash)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Normalize Rack-style nested query hashes into bracket notation when the
|
|
111
|
+
# schema expects bracket-named params or deepObject query params.
|
|
112
|
+
# Example: { "filter" => { "slug" => "/test" } } => { "filter[slug]" => "/test" }
|
|
113
|
+
# @param [Hash] raw_params
|
|
114
|
+
# @param [String] location
|
|
115
|
+
# @param [Array<OpenAPIParser::Schemas::Parameter>] params_for_location
|
|
116
|
+
# @return [Hash]
|
|
117
|
+
def normalize_raw_params(raw_params, location, params_for_location)
|
|
118
|
+
return raw_params unless location == 'query'
|
|
119
|
+
return raw_params unless raw_params.values.any? { |value| value.is_a?(Hash) }
|
|
120
|
+
return raw_params unless requires_query_param_flattening?(params_for_location)
|
|
121
|
+
|
|
122
|
+
normalized = Committee::Utils.indifferent_hash
|
|
123
|
+
|
|
124
|
+
raw_params.each do |key, value|
|
|
125
|
+
if should_flatten_query_param?(key, value, params_for_location)
|
|
126
|
+
flatten_nested_query_param(normalized, key.to_s, value)
|
|
127
|
+
else
|
|
128
|
+
normalized[key] = value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
normalized
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param [Array<OpenAPIParser::Schemas::Parameter>] params_for_location
|
|
136
|
+
# @return [Boolean]
|
|
137
|
+
def requires_query_param_flattening?(params_for_location)
|
|
138
|
+
params_for_location.any? { |param_def| param_def.style == 'deepObject' || param_def.name.include?('[') }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param [String, Symbol] key
|
|
142
|
+
# @param [Object] value
|
|
143
|
+
# @param [Array<OpenAPIParser::Schemas::Parameter>] params_for_location
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
def should_flatten_query_param?(key, value, params_for_location)
|
|
146
|
+
return false unless value.is_a?(Hash)
|
|
147
|
+
|
|
148
|
+
key_name = key.to_s
|
|
149
|
+
params_for_location.any? do |param_def|
|
|
150
|
+
param_def.name == key_name || param_def.name.start_with?("#{key_name}[")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param [Hash] result
|
|
155
|
+
# @param [String] prefix
|
|
156
|
+
# @param [Object] value
|
|
157
|
+
# @return [void]
|
|
158
|
+
def flatten_nested_query_param(result, prefix, value)
|
|
159
|
+
case value
|
|
160
|
+
when Hash
|
|
161
|
+
value.each do |child_key, child_value|
|
|
162
|
+
flatten_nested_query_param(result, "#{prefix}[#{child_key}]", child_value)
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
result[prefix] = value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Extract and deserialize a single parameter
|
|
170
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def Parameter definition
|
|
171
|
+
# @param [Hash] raw_params Raw parameters
|
|
172
|
+
# @return [Object] Deserialized value
|
|
173
|
+
def extract_and_deserialize(param_def, raw_params)
|
|
174
|
+
style = param_def.style || default_style(param_def.in)
|
|
175
|
+
explode = param_def.explode.nil? ? default_explode(style) : param_def.explode
|
|
176
|
+
|
|
177
|
+
case style
|
|
178
|
+
when 'form'
|
|
179
|
+
deserialize_form_style(param_def, raw_params, explode)
|
|
180
|
+
when 'simple'
|
|
181
|
+
deserialize_simple_style(param_def, raw_params, explode)
|
|
182
|
+
when 'label'
|
|
183
|
+
deserialize_label_style(param_def, raw_params, explode)
|
|
184
|
+
when 'matrix'
|
|
185
|
+
deserialize_matrix_style(param_def, raw_params, explode)
|
|
186
|
+
when 'spaceDelimited'
|
|
187
|
+
deserialize_space_delimited(param_def, raw_params, explode)
|
|
188
|
+
when 'pipeDelimited'
|
|
189
|
+
deserialize_pipe_delimited(param_def, raw_params, explode)
|
|
190
|
+
when 'deepObject'
|
|
191
|
+
deserialize_deep_object(param_def, raw_params)
|
|
192
|
+
else
|
|
193
|
+
# Unsupported style - return raw value
|
|
194
|
+
raw_params[param_def.name]
|
|
195
|
+
end
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
raise Committee::ParameterDeserializationError.new(param_def.name, style, raw_params[param_def.name], e.message)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Get default style for a parameter location
|
|
201
|
+
# @param [String] location Parameter location
|
|
202
|
+
# @return [String] Default style
|
|
203
|
+
def default_style(location)
|
|
204
|
+
case location
|
|
205
|
+
when 'query', 'cookie'
|
|
206
|
+
'form'
|
|
207
|
+
when 'path', 'header'
|
|
208
|
+
'simple'
|
|
209
|
+
else
|
|
210
|
+
'form'
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get default explode setting for a style
|
|
215
|
+
# @param [String] style Parameter style
|
|
216
|
+
# @return [Boolean] Default explode value
|
|
217
|
+
def default_explode(style)
|
|
218
|
+
style == 'form'
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Deserialize form style parameter
|
|
222
|
+
# Default for query and cookie parameters
|
|
223
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
224
|
+
# @param [Hash] raw_params
|
|
225
|
+
# @param [Boolean] explode
|
|
226
|
+
# @return [Object] Deserialized value
|
|
227
|
+
def deserialize_form_style(param_def, raw_params, explode)
|
|
228
|
+
param_name = param_def.name
|
|
229
|
+
schema = param_def.schema
|
|
230
|
+
return nil unless schema
|
|
231
|
+
|
|
232
|
+
case schema.type
|
|
233
|
+
when 'object'
|
|
234
|
+
deserialize_form_object(param_name, schema, raw_params, explode)
|
|
235
|
+
when 'array'
|
|
236
|
+
deserialize_form_array(param_name, schema, raw_params, explode)
|
|
237
|
+
else
|
|
238
|
+
# Primitive type - just return the value
|
|
239
|
+
raw_params[param_name]
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Deserialize form-style object
|
|
244
|
+
# @param [String] param_name
|
|
245
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
246
|
+
# @param [Hash] raw_params
|
|
247
|
+
# @param [Boolean] explode
|
|
248
|
+
# @return [Hash, nil] Deserialized object
|
|
249
|
+
def deserialize_form_object(param_name, schema, raw_params, explode)
|
|
250
|
+
if explode
|
|
251
|
+
# explode=true: object properties are separate parameters
|
|
252
|
+
# Example: ?role=admin&status=active → { role: "admin", status: "active" }
|
|
253
|
+
collect_object_properties(schema, raw_params, param_name)
|
|
254
|
+
else
|
|
255
|
+
# explode=false: comma-separated key,value pairs
|
|
256
|
+
# Example: ?id=role,admin,status,active → { role: "admin", status: "active" }
|
|
257
|
+
value = raw_params[param_name]
|
|
258
|
+
value ? parse_comma_separated_object(value) : nil
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Deserialize form-style array
|
|
263
|
+
# @param [String] param_name
|
|
264
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
265
|
+
# @param [Hash] raw_params
|
|
266
|
+
# @param [Boolean] explode
|
|
267
|
+
# @return [Array, nil] Deserialized array
|
|
268
|
+
def deserialize_form_array(param_name, schema, raw_params, explode)
|
|
269
|
+
value = raw_params[param_name]
|
|
270
|
+
return nil unless value
|
|
271
|
+
|
|
272
|
+
if explode
|
|
273
|
+
# explode=true: Rack already collects ?id=1&id=2 into an array
|
|
274
|
+
# Just ensure it's an array
|
|
275
|
+
Array(value)
|
|
276
|
+
else
|
|
277
|
+
# explode=false: comma-separated values
|
|
278
|
+
# Example: ?id=1,2,3 → ["1", "2", "3"]
|
|
279
|
+
value.is_a?(String) ? value.split(',') : Array(value)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Collect object properties from separate parameters (form explode=true)
|
|
284
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
285
|
+
# @param [Hash] raw_params
|
|
286
|
+
# @param [String] param_name Original parameter name (not used in exploded form)
|
|
287
|
+
# @return [Hash, nil] Collected object properties
|
|
288
|
+
def collect_object_properties(schema, raw_params, param_name)
|
|
289
|
+
result = Committee::Utils.indifferent_hash
|
|
290
|
+
properties = schema.properties || {}
|
|
291
|
+
|
|
292
|
+
properties.each do |prop_name, prop_schema|
|
|
293
|
+
# Look for property directly in raw_params (exploded form)
|
|
294
|
+
if raw_params.key?(prop_name)
|
|
295
|
+
result[prop_name] = raw_params[prop_name]
|
|
296
|
+
elsif raw_params.key?(prop_name.to_s)
|
|
297
|
+
result[prop_name] = raw_params[prop_name.to_s]
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
result.empty? ? nil : result
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Parse comma-separated object (form explode=false)
|
|
305
|
+
# Example: "role,admin,status,active" → { "role" => "admin", "status" => "active" }
|
|
306
|
+
# @param [String] value Comma-separated string
|
|
307
|
+
# @return [Hash] Parsed object
|
|
308
|
+
def parse_comma_separated_object(value)
|
|
309
|
+
parts = value.split(',')
|
|
310
|
+
result = Committee::Utils.indifferent_hash
|
|
311
|
+
|
|
312
|
+
# Parts should be in key,value,key,value format
|
|
313
|
+
(0...parts.length).step(2) do |i|
|
|
314
|
+
break if i + 1 >= parts.length
|
|
315
|
+
result[parts[i]] = parts[i + 1]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
result
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Deserialize deep object style (query parameters only)
|
|
322
|
+
# Example: ?filter[role]=admin&filter[status]=active → { filter: { role: "admin", status: "active" } }
|
|
323
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
324
|
+
# @param [Hash] raw_params
|
|
325
|
+
# @return [Hash, nil] Deserialized object
|
|
326
|
+
def deserialize_deep_object(param_def, raw_params)
|
|
327
|
+
param_name = param_def.name
|
|
328
|
+
prefix = "#{param_name}["
|
|
329
|
+
result = Committee::Utils.indifferent_hash
|
|
330
|
+
|
|
331
|
+
raw_params.each do |key, value|
|
|
332
|
+
key_str = key.to_s
|
|
333
|
+
if key_str.start_with?(prefix) && key_str.end_with?(']')
|
|
334
|
+
property_name = key_str[prefix.length...-1]
|
|
335
|
+
result[property_name] = value
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
result.empty? ? nil : result
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Deserialize simple style (path and header parameters)
|
|
343
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
344
|
+
# @param [Hash] raw_params
|
|
345
|
+
# @param [Boolean] explode
|
|
346
|
+
# @return [Object] Deserialized value
|
|
347
|
+
def deserialize_simple_style(param_def, raw_params, explode)
|
|
348
|
+
param_name = param_def.name
|
|
349
|
+
value = raw_params[param_name]
|
|
350
|
+
return nil unless value
|
|
351
|
+
|
|
352
|
+
schema = param_def.schema
|
|
353
|
+
return value unless schema
|
|
354
|
+
|
|
355
|
+
case schema.type
|
|
356
|
+
when 'array'
|
|
357
|
+
# Both explode true/false use comma separation for simple style
|
|
358
|
+
# Example: "1,2,3" → ["1", "2", "3"]
|
|
359
|
+
value.is_a?(String) ? value.split(',') : Array(value)
|
|
360
|
+
when 'object'
|
|
361
|
+
if explode
|
|
362
|
+
# explode=true: key=value,key2=value2
|
|
363
|
+
parse_key_value_pairs(value, ',', '=')
|
|
364
|
+
else
|
|
365
|
+
# explode=false: key,value,key2,value2
|
|
366
|
+
parse_comma_separated_object(value)
|
|
367
|
+
end
|
|
368
|
+
else
|
|
369
|
+
value
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Deserialize label style (path parameters)
|
|
374
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
375
|
+
# @param [Hash] raw_params
|
|
376
|
+
# @param [Boolean] explode
|
|
377
|
+
# @return [Object] Deserialized value
|
|
378
|
+
def deserialize_label_style(param_def, raw_params, explode)
|
|
379
|
+
param_name = param_def.name
|
|
380
|
+
value = raw_params[param_name]
|
|
381
|
+
return nil unless value
|
|
382
|
+
|
|
383
|
+
# Remove leading dot
|
|
384
|
+
value = value[1..-1] if value.start_with?('.')
|
|
385
|
+
|
|
386
|
+
schema = param_def.schema
|
|
387
|
+
return value unless schema
|
|
388
|
+
|
|
389
|
+
case schema.type
|
|
390
|
+
when 'array'
|
|
391
|
+
if explode
|
|
392
|
+
# explode=true: .3.4.5 → ["3", "4", "5"]
|
|
393
|
+
value.split('.')
|
|
394
|
+
else
|
|
395
|
+
# explode=false: .3,4,5 → ["3", "4", "5"]
|
|
396
|
+
value.split(',')
|
|
397
|
+
end
|
|
398
|
+
when 'object'
|
|
399
|
+
if explode
|
|
400
|
+
# explode=true: .role=admin.status=active
|
|
401
|
+
parse_key_value_pairs(value, '.', '=')
|
|
402
|
+
else
|
|
403
|
+
# explode=false: .role,admin,status,active
|
|
404
|
+
parse_comma_separated_object(value)
|
|
405
|
+
end
|
|
406
|
+
else
|
|
407
|
+
value
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Deserialize matrix style (path parameters)
|
|
412
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
413
|
+
# @param [Hash] raw_params
|
|
414
|
+
# @param [Boolean] explode
|
|
415
|
+
# @return [Object] Deserialized value
|
|
416
|
+
def deserialize_matrix_style(param_def, raw_params, explode)
|
|
417
|
+
param_name = param_def.name
|
|
418
|
+
value = raw_params[param_name]
|
|
419
|
+
return nil unless value
|
|
420
|
+
|
|
421
|
+
schema = param_def.schema
|
|
422
|
+
return value unless schema
|
|
423
|
+
|
|
424
|
+
case schema.type
|
|
425
|
+
when 'array'
|
|
426
|
+
if explode
|
|
427
|
+
# explode=true: ;id=3;id=4 (multiple occurrences)
|
|
428
|
+
# Rack should have already collected these into an array
|
|
429
|
+
Array(value)
|
|
430
|
+
else
|
|
431
|
+
# explode=false: ;id=3,4,5
|
|
432
|
+
extract_matrix_array_compact(value, param_name)
|
|
433
|
+
end
|
|
434
|
+
when 'object'
|
|
435
|
+
if explode
|
|
436
|
+
# explode=true: ;role=admin;status=active
|
|
437
|
+
extract_matrix_object_exploded(value, param_name)
|
|
438
|
+
else
|
|
439
|
+
# explode=false: ;id=role,admin,status,active
|
|
440
|
+
extract_matrix_object_compact(value, param_name)
|
|
441
|
+
end
|
|
442
|
+
else
|
|
443
|
+
# Primitive: ;id=5
|
|
444
|
+
extract_matrix_primitive(value, param_name)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Extract matrix-style array (explode=false)
|
|
449
|
+
# @param [String] value Matrix-style string
|
|
450
|
+
# @param [String] param_name Parameter name
|
|
451
|
+
# @return [Array] Extracted array
|
|
452
|
+
def extract_matrix_array_compact(value, param_name)
|
|
453
|
+
# ;id=3,4,5 → ["3", "4", "5"]
|
|
454
|
+
prefix = ";#{param_name}="
|
|
455
|
+
if value.start_with?(prefix)
|
|
456
|
+
value[prefix.length..-1].split(',')
|
|
457
|
+
else
|
|
458
|
+
[]
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Extract matrix-style object (explode=true)
|
|
463
|
+
# @param [String] value Matrix-style string
|
|
464
|
+
# @param [String] param_name Parameter name
|
|
465
|
+
# @return [Hash] Extracted object
|
|
466
|
+
def extract_matrix_object_exploded(value, param_name)
|
|
467
|
+
# ;role=admin;status=active → { "role" => "admin", "status" => "active" }
|
|
468
|
+
result = Committee::Utils.indifferent_hash
|
|
469
|
+
pairs = value.split(';').reject(&:empty?)
|
|
470
|
+
|
|
471
|
+
pairs.each do |pair|
|
|
472
|
+
next unless pair.include?('=')
|
|
473
|
+
key, val = pair.split('=', 2)
|
|
474
|
+
result[key] = val
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
result
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Extract matrix-style object (explode=false)
|
|
481
|
+
# @param [String] value Matrix-style string
|
|
482
|
+
# @param [String] param_name Parameter name
|
|
483
|
+
# @return [Hash] Extracted object
|
|
484
|
+
def extract_matrix_object_compact(value, param_name)
|
|
485
|
+
# ;id=role,admin,status,active → { "role" => "admin", "status" => "active" }
|
|
486
|
+
prefix = ";#{param_name}="
|
|
487
|
+
if value.start_with?(prefix)
|
|
488
|
+
parse_comma_separated_object(value[prefix.length..-1])
|
|
489
|
+
else
|
|
490
|
+
{}
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Extract matrix-style primitive value
|
|
495
|
+
# @param [String] value Matrix-style string
|
|
496
|
+
# @param [String] param_name Parameter name
|
|
497
|
+
# @return [String] Extracted value
|
|
498
|
+
def extract_matrix_primitive(value, param_name)
|
|
499
|
+
# ;id=5 → "5"
|
|
500
|
+
prefix = ";#{param_name}="
|
|
501
|
+
if value.start_with?(prefix)
|
|
502
|
+
value[prefix.length..-1]
|
|
503
|
+
else
|
|
504
|
+
value
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Deserialize space-delimited style (query parameters, arrays only)
|
|
509
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
510
|
+
# @param [Hash] raw_params
|
|
511
|
+
# @param [Boolean] explode
|
|
512
|
+
# @return [Array, nil] Deserialized array
|
|
513
|
+
def deserialize_space_delimited(param_def, raw_params, explode)
|
|
514
|
+
param_name = param_def.name
|
|
515
|
+
value = raw_params[param_name]
|
|
516
|
+
return nil unless value
|
|
517
|
+
|
|
518
|
+
# Example: "1 2 3" or "1%202%203" → ["1", "2", "3"]
|
|
519
|
+
value.is_a?(String) ? value.split(' ') : Array(value)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Deserialize pipe-delimited style (query parameters, arrays only)
|
|
523
|
+
# @param [OpenAPIParser::Schemas::Parameter] param_def
|
|
524
|
+
# @param [Hash] raw_params
|
|
525
|
+
# @param [Boolean] explode
|
|
526
|
+
# @return [Array, nil] Deserialized array
|
|
527
|
+
def deserialize_pipe_delimited(param_def, raw_params, explode)
|
|
528
|
+
param_name = param_def.name
|
|
529
|
+
value = raw_params[param_name]
|
|
530
|
+
return nil unless value
|
|
531
|
+
|
|
532
|
+
# Example: "1|2|3" → ["1", "2", "3"]
|
|
533
|
+
value.is_a?(String) ? value.split('|') : Array(value)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Parse key-value pairs with custom delimiters
|
|
537
|
+
# @param [String] value String containing key-value pairs
|
|
538
|
+
# @param [String] pair_delimiter Delimiter between pairs
|
|
539
|
+
# @param [String] kv_delimiter Delimiter between key and value
|
|
540
|
+
# @return [Hash] Parsed object
|
|
541
|
+
def parse_key_value_pairs(value, pair_delimiter, kv_delimiter)
|
|
542
|
+
result = Committee::Utils.indifferent_hash
|
|
543
|
+
pairs = value.split(pair_delimiter).reject(&:empty?)
|
|
544
|
+
|
|
545
|
+
pairs.each do |pair|
|
|
546
|
+
next unless pair.include?(kv_delimiter)
|
|
547
|
+
key, val = pair.split(kv_delimiter, 2)
|
|
548
|
+
result[key] = val
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
result
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
end
|
|
@@ -13,16 +13,30 @@ module Committee
|
|
|
13
13
|
@validate_success_only = validator_option.validate_success_only
|
|
14
14
|
@check_header = validator_option.check_header
|
|
15
15
|
@allow_empty_date_and_datetime = validator_option.allow_empty_date_and_datetime
|
|
16
|
+
@coerce_response_values = validator_option.coerce_response_values
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call(status, headers, response_data, strict)
|
|
19
|
-
return unless
|
|
20
|
+
return unless validate?(status)
|
|
20
21
|
|
|
21
|
-
validator_options = { allow_empty_date_and_datetime: @allow_empty_date_and_datetime }
|
|
22
|
+
validator_options = { allow_empty_date_and_datetime: @allow_empty_date_and_datetime, coerce_value: @coerce_response_values }
|
|
22
23
|
|
|
23
24
|
operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header, validator_options: validator_options)
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def validate?(status)
|
|
28
|
+
case status
|
|
29
|
+
when 204
|
|
30
|
+
false
|
|
31
|
+
when 200..299
|
|
32
|
+
true
|
|
33
|
+
when 304
|
|
34
|
+
false
|
|
35
|
+
else
|
|
36
|
+
!validate_success_only
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
private
|
|
27
41
|
|
|
28
42
|
attr_reader :operation_wrapper, :check_header
|
|
@@ -28,7 +28,7 @@ module Committee
|
|
|
28
28
|
|
|
29
29
|
parse_to_json = if validator_option.parse_response_by_content_type
|
|
30
30
|
content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') }
|
|
31
|
-
headers.fetch(content_type_key, nil)
|
|
31
|
+
Committee::SchemaValidator.json_media_type?(headers.fetch(content_type_key, nil))
|
|
32
32
|
else
|
|
33
33
|
true
|
|
34
34
|
end
|
|
@@ -43,9 +43,7 @@ module Committee
|
|
|
43
43
|
|
|
44
44
|
# TODO: refactoring name
|
|
45
45
|
strict = test_method
|
|
46
|
-
Committee::SchemaValidator::OpenAPI3::ResponseValidator.
|
|
47
|
-
new(@operation_object, validator_option).
|
|
48
|
-
call(status, headers, data, strict)
|
|
46
|
+
Committee::SchemaValidator::OpenAPI3::ResponseValidator.new(@operation_object, validator_option).call(status, headers, data, strict)
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def link_exist?
|
|
@@ -57,7 +55,6 @@ module Committee
|
|
|
57
55
|
attr_reader :validator_option
|
|
58
56
|
|
|
59
57
|
def coerce_path_params
|
|
60
|
-
return Committee::Utils.indifferent_hash unless validator_option.coerce_path_params
|
|
61
58
|
Committee::RequestUnpacker.indifferent_params(@operation_object.coerce_path_parameter(@validator_option))
|
|
62
59
|
end
|
|
63
60
|
|
|
@@ -85,14 +82,7 @@ module Committee
|
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
def request_unpack(request)
|
|
88
|
-
unpacker = Committee::RequestUnpacker.new(
|
|
89
|
-
allow_empty_date_and_datetime: validator_option.allow_empty_date_and_datetime,
|
|
90
|
-
allow_form_params: validator_option.allow_form_params,
|
|
91
|
-
allow_get_body: validator_option.allow_get_body,
|
|
92
|
-
allow_query_params: validator_option.allow_query_params,
|
|
93
|
-
allow_non_get_query_params: validator_option.allow_non_get_query_params,
|
|
94
|
-
optimistic_json: validator_option.optimistic_json,
|
|
95
|
-
)
|
|
85
|
+
unpacker = Committee::RequestUnpacker.new(allow_empty_date_and_datetime: validator_option.allow_empty_date_and_datetime, allow_form_params: validator_option.allow_form_params, allow_get_body: validator_option.allow_get_body, allow_query_params: validator_option.allow_query_params, allow_non_get_query_params: validator_option.allow_non_get_query_params, optimistic_json: validator_option.optimistic_json,)
|
|
96
86
|
|
|
97
87
|
request.env[validator_option.headers_key] = unpacker.unpack_headers(request)
|
|
98
88
|
|
|
@@ -102,6 +92,21 @@ module Committee
|
|
|
102
92
|
|
|
103
93
|
query_param = unpacker.unpack_query_params(request)
|
|
104
94
|
query_param.merge!(request_param) if request.get? && validator_option.allow_get_body
|
|
95
|
+
|
|
96
|
+
if @operation_object && validator_option.deserialize_parameters
|
|
97
|
+
deserializer = ParameterDeserializer.new(@operation_object.request_operation)
|
|
98
|
+
|
|
99
|
+
query_param = deserializer.deserialize_query_params(query_param)
|
|
100
|
+
|
|
101
|
+
path_param = request.env[validator_option.path_hash_key]
|
|
102
|
+
path_param = deserializer.deserialize_path_params(path_param)
|
|
103
|
+
request.env[validator_option.path_hash_key] = path_param
|
|
104
|
+
|
|
105
|
+
headers = request.env[validator_option.headers_key]
|
|
106
|
+
headers = deserializer.deserialize_headers(headers)
|
|
107
|
+
request.env[validator_option.headers_key] = headers
|
|
108
|
+
end
|
|
109
|
+
|
|
105
110
|
request.env[validator_option.query_hash_key] = query_param
|
|
106
111
|
end
|
|
107
112
|
|
|
@@ -125,5 +130,6 @@ end
|
|
|
125
130
|
|
|
126
131
|
require_relative "open_api_3/router"
|
|
127
132
|
require_relative "open_api_3/operation_wrapper"
|
|
133
|
+
require_relative "open_api_3/parameter_deserializer"
|
|
128
134
|
require_relative "open_api_3/request_validator"
|
|
129
135
|
require_relative "open_api_3/response_validator"
|