committee 5.6.2 → 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 +2 -1
- data/lib/committee/schema_validator/hyper_schema.rb +1 -1
- data/lib/committee/schema_validator/open_api_3/parameter_deserializer.rb +61 -0
- data/lib/committee/schema_validator/open_api_3.rb +1 -1
- data/lib/committee/schema_validator.rb +7 -0
- data/lib/committee/test/methods.rb +11 -0
- data/lib/committee/version.rb +1 -1
- data/test/middleware/request_validation_open_api_3_test.rb +87 -0
- data/test/middleware/response_validation_open_api_3_test.rb +7 -0
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +18 -0
- data/test/schema_validator_test.rb +33 -0
- data/test/test/methods_test.rb +16 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 694da9f92c8670bbe39fa64e209c43da029c58262ebbc249cff4512dee7e7aed
|
|
4
|
+
data.tar.gz: 2c3ffa46d5c827471545b585421a45e5c7781b9d1ddb1d861ba9bfd12a1fddd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc906473afe3beef06fe6f34b96ad3c29566761a1806dff3ed5fabe5d5b477bdbebb49216b5039e533316e0271957a30812f791c5e4ea8a306cf8613abdb8772
|
|
7
|
+
data.tar.gz: ffa5ad6a180270f703d13fa2dacebe534c0a9c76bc5be9d4a27eac1f07e9592813bfa81d74712b63d5ff9502ebfde97cd171cca3abb2376f4f2a9865d4f11614
|
|
@@ -8,7 +8,8 @@ module Committee
|
|
|
8
8
|
true
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Historically named for form params, but in OpenAPI 3 this flag controls
|
|
12
|
+
# request body coercion more generally.
|
|
12
13
|
def default_coerce_form_params
|
|
13
14
|
true
|
|
14
15
|
end
|
|
@@ -31,7 +31,7 @@ module Committee
|
|
|
31
31
|
elsif !full_body.empty?
|
|
32
32
|
parse_to_json = if validator_option.parse_response_by_content_type
|
|
33
33
|
content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') }
|
|
34
|
-
|
|
34
|
+
Committee::SchemaValidator.json_media_type?(headers.fetch(content_type_key, nil))
|
|
35
35
|
else
|
|
36
36
|
true
|
|
37
37
|
end
|
|
@@ -52,6 +52,8 @@ module Committee
|
|
|
52
52
|
# If no parameters are defined for this location, return raw params as-is
|
|
53
53
|
return raw_params if params_for_location.empty?
|
|
54
54
|
|
|
55
|
+
raw_params = normalize_raw_params(raw_params, location, params_for_location)
|
|
56
|
+
|
|
55
57
|
# Collect parameter names that will be deserialized
|
|
56
58
|
# This includes both the parameter name and any properties (for exploded objects)
|
|
57
59
|
deserialized_keys = Set.new
|
|
@@ -105,6 +107,65 @@ module Committee
|
|
|
105
107
|
Committee::Utils.indifferent_hash.merge(hash)
|
|
106
108
|
end
|
|
107
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
|
+
|
|
108
169
|
# Extract and deserialize a single parameter
|
|
109
170
|
# @param [OpenAPIParser::Schemas::Parameter] param_def Parameter definition
|
|
110
171
|
# @param [Hash] raw_params Raw parameters
|
|
@@ -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
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Committee
|
|
4
4
|
module SchemaValidator
|
|
5
|
+
JSON_MEDIA_TYPE_PATTERN = %r{\Aapplication/(?:.+\+)?json\z}.freeze
|
|
6
|
+
|
|
5
7
|
class << self
|
|
6
8
|
def request_media_type(request)
|
|
7
9
|
Rack::MediaType.type(request.env['CONTENT_TYPE'])
|
|
8
10
|
end
|
|
9
11
|
|
|
12
|
+
def json_media_type?(content_type)
|
|
13
|
+
normalized_content_type = Rack::MediaType.type(content_type)
|
|
14
|
+
normalized_content_type&.match?(JSON_MEDIA_TYPE_PATTERN) || false
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
# @param [String] prefix
|
|
11
18
|
# @return [Regexp]
|
|
12
19
|
def build_prefix_regexp(prefix)
|
|
@@ -23,6 +23,8 @@ module Committee
|
|
|
23
23
|
schema_validator.request_validate(request_object)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
increment_assertion_count
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def assert_response_schema_confirm(expected_status = nil)
|
|
@@ -46,6 +48,8 @@ module Committee
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
schema_validator.response_validate(status, headers, [body], true) if validate_response?(status)
|
|
51
|
+
|
|
52
|
+
increment_assertion_count
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
def committee_options
|
|
@@ -90,6 +94,13 @@ module Committee
|
|
|
90
94
|
|
|
91
95
|
private
|
|
92
96
|
|
|
97
|
+
# assert_*_schema_confirm signal failure by raising, not via `assert`,
|
|
98
|
+
# so Minitest would report "Test is missing assertions" on success.
|
|
99
|
+
# Bump the counter explicitly; no-op outside Minitest (e.g. RSpec).
|
|
100
|
+
def increment_assertion_count
|
|
101
|
+
assert true if respond_to?(:assertions)
|
|
102
|
+
end
|
|
103
|
+
|
|
93
104
|
# Temporarily adds dummy values for excepted parameters during validation
|
|
94
105
|
# @see ExceptParameter
|
|
95
106
|
def with_except_params(except)
|
data/lib/committee/version.rb
CHANGED
|
@@ -660,6 +660,44 @@ describe Committee::Middleware::RequestValidation do
|
|
|
660
660
|
end
|
|
661
661
|
end
|
|
662
662
|
|
|
663
|
+
describe 'bracket-style query params' do
|
|
664
|
+
it 'validates query params declared with bracket notation names' do
|
|
665
|
+
check_parameter = lambda { |env|
|
|
666
|
+
assert_equal '/test', env['committee.query_hash']['filter[slug]']
|
|
667
|
+
refute env['committee.query_hash'].key?('filter')
|
|
668
|
+
[200, {}, []]
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@app = new_rack_app_with_lambda(check_parameter, schema: query_param_schema(bracket_notation_query_parameter))
|
|
672
|
+
|
|
673
|
+
get '/events?filter[slug]=%2Ftest'
|
|
674
|
+
|
|
675
|
+
assert_equal 200, last_response.status
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
it 'rejects unknown nested query params with strict_query_params' do
|
|
679
|
+
@app = new_rack_app(schema: query_param_schema(bracket_notation_query_parameter), strict_query_params: true)
|
|
680
|
+
|
|
681
|
+
get '/events?filter[slug]=%2Ftest&filter[status]=active'
|
|
682
|
+
|
|
683
|
+
assert_equal 400, last_response.status
|
|
684
|
+
assert_match(/filter\[status\]/, last_response.body)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
it 'continues to support deepObject query params from Rack nested hashes' do
|
|
688
|
+
check_parameter = lambda { |env|
|
|
689
|
+
assert_equal '/test', env['committee.query_hash']['filter']['slug']
|
|
690
|
+
[200, {}, []]
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@app = new_rack_app_with_lambda(check_parameter, schema: query_param_schema(deep_object_query_parameter))
|
|
694
|
+
|
|
695
|
+
get '/events?filter[slug]=%2Ftest'
|
|
696
|
+
|
|
697
|
+
assert_equal 200, last_response.status
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
663
701
|
private
|
|
664
702
|
|
|
665
703
|
def new_rack_app(options = {})
|
|
@@ -674,4 +712,53 @@ describe Committee::Middleware::RequestValidation do
|
|
|
674
712
|
run check_lambda
|
|
675
713
|
}
|
|
676
714
|
end
|
|
715
|
+
|
|
716
|
+
def query_param_schema(parameter)
|
|
717
|
+
Committee::Drivers.load_from_data(query_param_document(parameter), nil, parser_options: { strict_reference_validation: true })
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def bracket_notation_query_parameter
|
|
721
|
+
{
|
|
722
|
+
'name' => 'filter[slug]',
|
|
723
|
+
'in' => 'query',
|
|
724
|
+
'required' => true,
|
|
725
|
+
'schema' => { 'type' => 'string' },
|
|
726
|
+
}
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def deep_object_query_parameter
|
|
730
|
+
{
|
|
731
|
+
'name' => 'filter',
|
|
732
|
+
'in' => 'query',
|
|
733
|
+
'required' => true,
|
|
734
|
+
'style' => 'deepObject',
|
|
735
|
+
'explode' => true,
|
|
736
|
+
'schema' => {
|
|
737
|
+
'type' => 'object',
|
|
738
|
+
'required' => ['slug'],
|
|
739
|
+
'properties' => {
|
|
740
|
+
'slug' => { 'type' => 'string' },
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
}
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def query_param_document(parameter)
|
|
747
|
+
{
|
|
748
|
+
'openapi' => '3.0.3',
|
|
749
|
+
'info' => { 'title' => 'test', 'version' => '1.0.0' },
|
|
750
|
+
'paths' => {
|
|
751
|
+
'/events' => {
|
|
752
|
+
'get' => {
|
|
753
|
+
'parameters' => [parameter],
|
|
754
|
+
'responses' => {
|
|
755
|
+
'200' => {
|
|
756
|
+
'description' => 'ok',
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
}
|
|
763
|
+
end
|
|
677
764
|
end
|
|
@@ -33,6 +33,13 @@ describe Committee::Middleware::ResponseValidation do
|
|
|
33
33
|
assert_equal 200, last_response.status
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
it "passes through a valid response with a +json content-type" do
|
|
37
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/problem+json; charset=utf-8" }, schema: open_api_3_schema, parse_response_by_content_type: true,)
|
|
38
|
+
|
|
39
|
+
get "/characters"
|
|
40
|
+
assert_equal 200, last_response.status
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
it "passes through a invalid json" do
|
|
37
44
|
@app = new_response_rack("not_json", {}, schema: open_api_3_schema)
|
|
38
45
|
|
|
@@ -204,6 +204,24 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
|
|
|
204
204
|
@path = '/characters'
|
|
205
205
|
operation_object.validate_request_params({}, { "limit" => "1" }, {}, HEADER, query_coercion_enabled)
|
|
206
206
|
end
|
|
207
|
+
|
|
208
|
+
it 'uses coerce_form_params for JSON request bodies too' do
|
|
209
|
+
@path = '/validate'
|
|
210
|
+
@method = 'post'
|
|
211
|
+
|
|
212
|
+
body_coercion_disabled = Committee::SchemaValidator::Option.new({ coerce_form_params: false }, open_api_3_schema, :open_api_3)
|
|
213
|
+
body_coercion_enabled = Committee::SchemaValidator::Option.new({ coerce_form_params: true }, open_api_3_schema, :open_api_3)
|
|
214
|
+
|
|
215
|
+
error = assert_raises(Committee::InvalidRequest) do
|
|
216
|
+
operation_object.validate_request_params({}, {}, { "integer" => "1" }, HEADER, body_coercion_disabled)
|
|
217
|
+
end
|
|
218
|
+
assert_match(/expected integer, but received String: "1"/i, error.message)
|
|
219
|
+
|
|
220
|
+
body_params = { "integer" => "1" }
|
|
221
|
+
operation_object.validate_request_params({}, {}, body_params, HEADER, body_coercion_enabled)
|
|
222
|
+
|
|
223
|
+
assert_kind_of(Integer, body_params["integer"])
|
|
224
|
+
end
|
|
207
225
|
end
|
|
208
226
|
end
|
|
209
227
|
end
|
|
@@ -21,6 +21,39 @@ describe Committee::SchemaValidator do
|
|
|
21
21
|
assert_equal 'multipart/form-data', media_type
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
it "detects application/json as a JSON media type" do
|
|
25
|
+
assert_equal true, Committee::SchemaValidator.json_media_type?("application/json")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "detects application/problem+json with parameters as a JSON media type" do
|
|
29
|
+
assert_equal true, Committee::SchemaValidator.json_media_type?("application/problem+json; charset=utf-8")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "detects application/vnd.api+json as a JSON media type" do
|
|
33
|
+
assert_equal true, Committee::SchemaValidator.json_media_type?("application/vnd.api+json")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "does not detect application/x-ndjson as a JSON media type" do
|
|
37
|
+
assert_equal false, Committee::SchemaValidator.json_media_type?("application/x-ndjson")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "does not detect non-JSON content types as JSON media types" do
|
|
41
|
+
assert_equal false, Committee::SchemaValidator.json_media_type?("test/csv")
|
|
42
|
+
assert_equal false, Committee::SchemaValidator.json_media_type?(nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "parses +json responses in the HyperSchema validator" do
|
|
46
|
+
schema = Committee::Drivers::OpenAPI2::Driver.new.parse(open_api_2_data)
|
|
47
|
+
validator_option = Committee::SchemaValidator::Option.new({ parse_response_by_content_type: true }, schema, :hyper_schema)
|
|
48
|
+
router = Committee::SchemaValidator::HyperSchema::Router.new(schema, validator_option)
|
|
49
|
+
request = Rack::Request.new({ "REQUEST_METHOD" => "GET", "PATH_INFO" => "/api/pets", "rack.input" => StringIO.new("") })
|
|
50
|
+
validator = Committee::SchemaValidator::HyperSchema.new(router, request, validator_option)
|
|
51
|
+
|
|
52
|
+
validator.link.media_type = "application/vnd.api+json"
|
|
53
|
+
|
|
54
|
+
validator.response_validate(200, { "Content-Type" => "application/vnd.api+json" }, [JSON.generate([ValidPet])])
|
|
55
|
+
end
|
|
56
|
+
|
|
24
57
|
it "builds prefix regexp with a path segment boundary" do
|
|
25
58
|
regexp = Committee::SchemaValidator.build_prefix_regexp("/v1")
|
|
26
59
|
|
data/test/test/methods_test.rb
CHANGED
|
@@ -151,6 +151,14 @@ describe Committee::Test::Methods do
|
|
|
151
151
|
assert_request_schema_confirm
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
it "increments the Minitest assertion count on success" do
|
|
155
|
+
@app = new_rack_app
|
|
156
|
+
get "/characters"
|
|
157
|
+
before_count = assertions
|
|
158
|
+
assert_request_schema_confirm
|
|
159
|
+
assert_equal before_count + 1, assertions
|
|
160
|
+
end
|
|
161
|
+
|
|
154
162
|
it "not exist required" do
|
|
155
163
|
@app = new_rack_app
|
|
156
164
|
get "/validate", { "query_string" => "query", "query_integer_list" => [1, 2] }
|
|
@@ -397,6 +405,14 @@ describe Committee::Test::Methods do
|
|
|
397
405
|
assert_response_schema_confirm(200)
|
|
398
406
|
end
|
|
399
407
|
|
|
408
|
+
it "increments the Minitest assertion count on success" do
|
|
409
|
+
@app = new_rack_app(JSON.generate(@correct_response))
|
|
410
|
+
get "/characters"
|
|
411
|
+
before_count = assertions
|
|
412
|
+
assert_response_schema_confirm(200)
|
|
413
|
+
assert_equal before_count + 1, assertions
|
|
414
|
+
end
|
|
415
|
+
|
|
400
416
|
it "detects an invalid response Content-Type" do
|
|
401
417
|
@app = new_rack_app(JSON.generate([@correct_response]), {})
|
|
402
418
|
get "/characters"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: committee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.6.
|
|
4
|
+
version: 5.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brandur
|
|
@@ -277,7 +277,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
277
277
|
- !ruby/object:Gem::Version
|
|
278
278
|
version: '0'
|
|
279
279
|
requirements: []
|
|
280
|
-
rubygems_version:
|
|
280
|
+
rubygems_version: 3.6.9
|
|
281
281
|
specification_version: 4
|
|
282
282
|
summary: A collection of Rack middleware to support JSON Schema.
|
|
283
283
|
test_files: []
|