committee 5.6.1 → 5.6.2

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/committee/drivers/open_api_3/driver.rb +5 -0
  3. data/lib/committee/drivers.rb +2 -3
  4. data/lib/committee/errors.rb +11 -0
  5. data/lib/committee/middleware/base.rb +11 -5
  6. data/lib/committee/middleware/options/base.rb +107 -0
  7. data/lib/committee/middleware/options/request_validation.rb +80 -0
  8. data/lib/committee/middleware/options/response_validation.rb +46 -0
  9. data/lib/committee/middleware/options.rb +12 -0
  10. data/lib/committee/middleware/request_validation.rb +7 -1
  11. data/lib/committee/middleware/response_validation.rb +6 -2
  12. data/lib/committee/middleware.rb +1 -0
  13. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +1 -3
  14. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +14 -1
  15. data/lib/committee/schema_validator/hyper_schema/router.rb +1 -1
  16. data/lib/committee/schema_validator/hyper_schema.rb +2 -13
  17. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +43 -13
  18. data/lib/committee/schema_validator/open_api_3/parameter_deserializer.rb +495 -0
  19. data/lib/committee/schema_validator/open_api_3/response_validator.rb +16 -2
  20. data/lib/committee/schema_validator/open_api_3.rb +18 -12
  21. data/lib/committee/schema_validator/option.rb +6 -17
  22. data/lib/committee/schema_validator.rb +5 -1
  23. data/lib/committee/test/except_parameter.rb +416 -0
  24. data/lib/committee/test/methods.rb +27 -2
  25. data/lib/committee/version.rb +1 -1
  26. data/lib/committee.rb +1 -1
  27. data/test/drivers/open_api_2/driver_test.rb +4 -16
  28. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +4 -50
  29. data/test/drivers_test.rb +35 -21
  30. data/test/middleware/options/base_test.rb +120 -0
  31. data/test/middleware/options/request_validation_test.rb +177 -0
  32. data/test/middleware/options/response_validation_test.rb +121 -0
  33. data/test/middleware/request_validation_open_api_3_test.rb +113 -80
  34. data/test/middleware/request_validation_test.rb +13 -70
  35. data/test/middleware/response_validation_open_api_3_test.rb +33 -17
  36. data/test/middleware/response_validation_test.rb +3 -14
  37. data/test/request_unpacker_test.rb +2 -10
  38. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +1 -37
  39. data/test/schema_validator/hyper_schema/request_validator_test.rb +6 -30
  40. data/test/schema_validator/hyper_schema/router_test.rb +5 -0
  41. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +1 -37
  42. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +40 -43
  43. data/test/schema_validator/open_api_3/parameter_deserializer_test.rb +457 -0
  44. data/test/schema_validator/open_api_3/request_validator_test.rb +1 -2
  45. data/test/schema_validator/open_api_3/response_validator_test.rb +3 -11
  46. data/test/schema_validator_test.rb +8 -0
  47. data/test/test/methods_test.rb +222 -105
  48. data/test/test/schema_coverage_test.rb +8 -155
  49. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07fd25d2379c9e5d324556185a1071995f394712a778f4d8ee2afda016601645
4
- data.tar.gz: dd86ac014d3b34fc1f2d03456fe74190756084c8432858d20e01717562ff7c49
3
+ metadata.gz: 6f5e2c05d7caa0327f1f9aab8eb4e70fde475df991c89d2969cf1e64fbaf2ab4
4
+ data.tar.gz: b8e55ce938ae8276b2469cef2a5f451b326fcbd94ab10a0957a0c8908307707d
5
5
  SHA512:
6
- metadata.gz: 2226c6247449ba57f98ec7fdf69c287d797ff7aabf92de710e91d7d080e3bc8ec65b9b88a5304e549d13f023c6238343c1971f801784f8b4fc2dce6587841e49
7
- data.tar.gz: 589bf4caf9cdd2ded55739c90de5db53ebd21e52b66f30d4d883289349af9553fd619fd5d4b7328c0c85e5402f274018b00d6a7ed55e2fcc608157c99d87b2ec
6
+ metadata.gz: 48819e9df56d4ad78afc3e4ccd6992eba3570a3e3ec39570354b8ab3777e73d4ef75f5554cfb2e3bf173b91d645c0015173c465e64712f87d4ef22ad02738b7e
7
+ data.tar.gz: f3e72af3ca96804db1b07f0b28002367bee84d6557f1725c0244fee839757a940c7aa89e6373952559109c273f5ec721759be89d8f98517234bd3d00bdba17ce
@@ -33,6 +33,11 @@ module Committee
33
33
  false
34
34
  end
35
35
 
36
+ # Whether to deserialize parameters based on OpenAPI 3 style/explode settings
37
+ def default_deserialize_parameters
38
+ true
39
+ end
40
+
36
41
  def name
37
42
  :open_api_3
38
43
  end
@@ -61,9 +61,7 @@ module Committee
61
61
  # if it is not explicitly set. See: https://github.com/interagent/committee/issues/343#issuecomment-997400329
62
62
  opts = parser_options.dup
63
63
 
64
- Committee.warn_deprecated_until_6(!opts.key?(:strict_reference_validation), 'openapi_parser will default to strict reference validation ' +
65
- 'from next version. Pass config `strict_reference_validation: true` (or false, if you must) ' +
66
- 'to quiet this warning.')
64
+ Committee.warn_deprecated_until_6(!opts.key?(:strict_reference_validation), 'openapi_parser will default to strict reference validation ' + 'from next version. Pass config `strict_reference_validation: true` (or false, if you must) ' + 'to quiet this warning.')
67
65
  opts[:strict_reference_validation] ||= false
68
66
 
69
67
  openapi = OpenAPIParser.parse_with_filepath(hash, schema_path, opts)
@@ -91,6 +89,7 @@ module Committee
91
89
 
92
90
  def cache_key(schema_path, parser_options)
93
91
  [
92
+ File.expand_path(schema_path),
94
93
  File.exist?(schema_path) ? Digest::MD5.hexdigest(File.read(schema_path)) : nil,
95
94
  parser_options.hash,
96
95
  ].join('_')
@@ -33,4 +33,15 @@ module Committee
33
33
 
34
34
  class OpenAPI3Unsupported < Error
35
35
  end
36
+
37
+ class ParameterDeserializationError < InvalidRequest
38
+ attr_reader :parameter_name, :style, :raw_value
39
+
40
+ def initialize(param_name, style, raw_value, message)
41
+ @parameter_name = param_name
42
+ @style = style
43
+ @raw_value = raw_value
44
+ super("Parameter '#{param_name}' (style: #{style}): #{message}")
45
+ end
46
+ end
36
47
  end
@@ -5,16 +5,17 @@ module Committee
5
5
  class Base
6
6
  def initialize(app, options = {})
7
7
  @app = app
8
+ @options = build_options(options)
8
9
 
9
- @error_class = options.fetch(:error_class, Committee::ValidationError)
10
- @error_handler = options[:error_handler]
11
- @ignore_error = options.fetch(:ignore_error, false)
10
+ @error_class = @options.error_class
11
+ @error_handler = @options.error_handler
12
+ @ignore_error = @options.ignore_error
12
13
 
13
- @raise = options[:raise]
14
+ @raise = @options.raise_error
14
15
  @schema = self.class.get_schema(options)
15
16
 
16
17
  @router = @schema.build_router(options)
17
- @accept_request_filter = options[:accept_request_filter] || ->(_) { true }
18
+ @accept_request_filter = @options.accept_request_filter
18
19
  end
19
20
 
20
21
  def call(env)
@@ -49,6 +50,11 @@ module Committee
49
50
 
50
51
  private
51
52
 
53
+ # Subclasses should override this method to use their specific Options class
54
+ def build_options(options)
55
+ Options::Base.from(options)
56
+ end
57
+
52
58
  def build_schema_validator(request)
53
59
  @router.build_schema_validator(request)
54
60
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Middleware
5
+ module Options
6
+ class Base
7
+ # Common options
8
+ attr_reader :schema
9
+ attr_reader :schema_path
10
+ attr_reader :error_class
11
+ attr_reader :error_handler
12
+ attr_reader :raise_error
13
+ attr_reader :ignore_error
14
+ attr_reader :prefix
15
+ attr_reader :accept_request_filter
16
+
17
+ # Schema loading options
18
+ attr_reader :strict_reference_validation
19
+
20
+ def initialize(options = {})
21
+ @original_options = options.dup.freeze
22
+
23
+ # Schema related
24
+ @schema = options[:schema]
25
+ @schema_path = options[:schema_path]
26
+ @strict_reference_validation = options.fetch(:strict_reference_validation, false)
27
+
28
+ # Error handling
29
+ @error_class = options.fetch(:error_class, Committee::ValidationError)
30
+ @error_handler = options[:error_handler]
31
+ @raise_error = options[:raise] || false
32
+ @ignore_error = options.fetch(:ignore_error, false)
33
+
34
+ # Routing
35
+ @prefix = options[:prefix]
36
+ @accept_request_filter = options[:accept_request_filter] || ->(_) { true }
37
+
38
+ validate!
39
+ end
40
+
41
+ # Allow Hash-like access for backward compatibility
42
+ def [](key)
43
+ to_h[key]
44
+ end
45
+
46
+ def fetch(key, default = nil)
47
+ to_h.fetch(key, default)
48
+ end
49
+
50
+ def to_h
51
+ @to_h ||= build_hash
52
+ end
53
+
54
+ # Create an Option object from Hash options
55
+ # Returns the object as-is if already an Option object
56
+ def self.from(options)
57
+ case options
58
+ when self
59
+ options
60
+ when Hash
61
+ new(options)
62
+ else
63
+ raise ArgumentError, "options must be a Hash or #{name}"
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def validate!
70
+ validate_error_class!
71
+ validate_error_handler!
72
+ validate_schema_source!
73
+ validate_accept_request_filter!
74
+ end
75
+
76
+ def validate_error_handler!
77
+ return if @error_handler.nil? || @error_handler.respond_to?(:call)
78
+
79
+ raise ArgumentError, "error_handler must respond to #call"
80
+ end
81
+
82
+ def validate_error_class!
83
+ return if @error_class.is_a?(Class)
84
+
85
+ raise ArgumentError, "error_class must be a Class"
86
+ end
87
+
88
+ def validate_schema_source!
89
+ return if @schema || @schema_path
90
+
91
+ raise ArgumentError, "Committee: need option `schema` or `schema_path`"
92
+ end
93
+
94
+ def validate_accept_request_filter!
95
+ return if @accept_request_filter.respond_to?(:call)
96
+
97
+ raise ArgumentError, "accept_request_filter must respond to #call"
98
+ end
99
+
100
+ def build_hash
101
+ # Start with original options to preserve any options not explicitly handled
102
+ @original_options.dup.merge(schema: @schema, schema_path: @schema_path, strict_reference_validation: @strict_reference_validation, error_class: @error_class, error_handler: @error_handler, raise: @raise_error, ignore_error: @ignore_error, prefix: @prefix, accept_request_filter: @accept_request_filter)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Middleware
5
+ module Options
6
+ class RequestValidation < Base
7
+ # RequestValidation specific options
8
+ attr_reader :strict
9
+ attr_reader :coerce_date_times
10
+ attr_reader :coerce_recursive
11
+ attr_reader :coerce_form_params
12
+ attr_reader :coerce_path_params
13
+ attr_reader :coerce_query_params
14
+ attr_reader :allow_form_params
15
+ attr_reader :allow_query_params
16
+ attr_reader :allow_get_body
17
+ attr_reader :allow_non_get_query_params
18
+ attr_reader :check_content_type
19
+ attr_reader :check_header
20
+ attr_reader :optimistic_json
21
+ attr_reader :request_body_hash_key
22
+ attr_reader :query_hash_key
23
+ attr_reader :path_hash_key
24
+ attr_reader :headers_key
25
+ attr_reader :params_key
26
+ attr_reader :allow_blank_structures
27
+ attr_reader :allow_empty_date_and_datetime
28
+ attr_reader :parameter_overwrite_by_rails_rule
29
+ attr_reader :deserialize_parameters
30
+
31
+ # Default values
32
+ DEFAULTS = { strict: false, coerce_recursive: true, allow_get_body: false, check_content_type: true, check_header: true, optimistic_json: false, allow_form_params: true, allow_query_params: true, allow_non_get_query_params: false, allow_blank_structures: false, allow_empty_date_and_datetime: false, parameter_overwrite_by_rails_rule: true, request_body_hash_key: "committee.request_body_hash", query_hash_key: "committee.query_hash", path_hash_key: "committee.path_hash", headers_key: "committee.headers", params_key: "committee.params" }.freeze
33
+
34
+ def initialize(options = {})
35
+ super(options)
36
+
37
+ # Validation related
38
+ @strict = options.fetch(:strict, DEFAULTS[:strict])
39
+ @check_content_type = options.fetch(:check_content_type, DEFAULTS[:check_content_type])
40
+ @check_header = options.fetch(:check_header, DEFAULTS[:check_header])
41
+ @optimistic_json = options.fetch(:optimistic_json, DEFAULTS[:optimistic_json])
42
+
43
+ # Type coercion related
44
+ @coerce_date_times = options[:coerce_date_times]
45
+ @coerce_recursive = options.fetch(:coerce_recursive, DEFAULTS[:coerce_recursive])
46
+ @coerce_form_params = options[:coerce_form_params]
47
+ @coerce_path_params = options[:coerce_path_params]
48
+ @coerce_query_params = options[:coerce_query_params]
49
+
50
+ # Parameter permission related
51
+ @allow_form_params = options.fetch(:allow_form_params, DEFAULTS[:allow_form_params])
52
+ @allow_query_params = options.fetch(:allow_query_params, DEFAULTS[:allow_query_params])
53
+ @allow_get_body = options.fetch(:allow_get_body, DEFAULTS[:allow_get_body])
54
+ @allow_non_get_query_params = options.fetch(:allow_non_get_query_params, DEFAULTS[:allow_non_get_query_params])
55
+ @allow_blank_structures = options.fetch(:allow_blank_structures, DEFAULTS[:allow_blank_structures])
56
+ @allow_empty_date_and_datetime = options.fetch(:allow_empty_date_and_datetime, DEFAULTS[:allow_empty_date_and_datetime])
57
+
58
+ # Rails compatibility
59
+ @parameter_overwrite_by_rails_rule = options.fetch(:parameter_overwrite_by_rails_rule, DEFAULTS[:parameter_overwrite_by_rails_rule])
60
+
61
+ # Deserialization
62
+ @deserialize_parameters = options[:deserialize_parameters]
63
+
64
+ # Hash keys
65
+ @request_body_hash_key = options.fetch(:request_body_hash_key, DEFAULTS[:request_body_hash_key])
66
+ @query_hash_key = options.fetch(:query_hash_key, DEFAULTS[:query_hash_key])
67
+ @path_hash_key = options.fetch(:path_hash_key, DEFAULTS[:path_hash_key])
68
+ @headers_key = options.fetch(:headers_key, DEFAULTS[:headers_key])
69
+ @params_key = options.fetch(:params_key, DEFAULTS[:params_key])
70
+ end
71
+
72
+ private
73
+
74
+ def build_hash
75
+ super.merge(strict: @strict, coerce_date_times: @coerce_date_times, coerce_recursive: @coerce_recursive, coerce_form_params: @coerce_form_params, coerce_path_params: @coerce_path_params, coerce_query_params: @coerce_query_params, allow_form_params: @allow_form_params, allow_query_params: @allow_query_params, allow_get_body: @allow_get_body, allow_non_get_query_params: @allow_non_get_query_params, allow_blank_structures: @allow_blank_structures, allow_empty_date_and_datetime: @allow_empty_date_and_datetime, parameter_overwrite_by_rails_rule: @parameter_overwrite_by_rails_rule, deserialize_parameters: @deserialize_parameters, check_content_type: @check_content_type, check_header: @check_header, optimistic_json: @optimistic_json, request_body_hash_key: @request_body_hash_key, query_hash_key: @query_hash_key, path_hash_key: @path_hash_key, headers_key: @headers_key, params_key: @params_key)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Middleware
5
+ module Options
6
+ class ResponseValidation < Base
7
+ # ResponseValidation specific options
8
+ attr_reader :strict
9
+ attr_reader :validate_success_only
10
+ attr_reader :parse_response_by_content_type
11
+ attr_reader :coerce_response_values
12
+ attr_reader :streaming_content_parsers
13
+
14
+ # Default values
15
+ DEFAULTS = { strict: false, validate_success_only: true, parse_response_by_content_type: true, coerce_response_values: false }.freeze
16
+
17
+ def initialize(options = {})
18
+ super(options)
19
+
20
+ # Validation related
21
+ @strict = options.fetch(:strict, DEFAULTS[:strict])
22
+ @validate_success_only = options.fetch(:validate_success_only, DEFAULTS[:validate_success_only])
23
+ @parse_response_by_content_type = options.fetch(:parse_response_by_content_type, DEFAULTS[:parse_response_by_content_type])
24
+ @coerce_response_values = options.fetch(:coerce_response_values, DEFAULTS[:coerce_response_values])
25
+
26
+ # Streaming
27
+ @streaming_content_parsers = options[:streaming_content_parsers] || {}
28
+
29
+ validate_response_validation_options!
30
+ end
31
+
32
+ private
33
+
34
+ def validate_response_validation_options!
35
+ return if @streaming_content_parsers.is_a?(Hash)
36
+
37
+ raise ArgumentError, "streaming_content_parsers must be a Hash"
38
+ end
39
+
40
+ def build_hash
41
+ super.merge(strict: @strict, validate_success_only: @validate_success_only, parse_response_by_content_type: @parse_response_by_content_type, coerce_response_values: @coerce_response_values, streaming_content_parsers: @streaming_content_parsers)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Middleware
5
+ module Options
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative "options/base"
11
+ require_relative "options/request_validation"
12
+ require_relative "options/response_validation"
@@ -6,7 +6,13 @@ module Committee
6
6
  def initialize(app, options = {})
7
7
  super
8
8
 
9
- @strict = options[:strict]
9
+ @strict = @options.strict
10
+ end
11
+
12
+ private
13
+
14
+ def build_options(options)
15
+ Options::RequestValidation.from(options)
10
16
  end
11
17
 
12
18
  def handle(request)
@@ -7,9 +7,9 @@ module Committee
7
7
 
8
8
  def initialize(app, options = {})
9
9
  super
10
- @strict = options[:strict]
10
+ @strict = @options.strict
11
11
  @validate_success_only = @schema.validator_option.validate_success_only
12
- @streaming_content_parsers = options[:streaming_content_parsers] || {}
12
+ @streaming_content_parsers = @options.streaming_content_parsers
13
13
  end
14
14
 
15
15
  def handle(request)
@@ -63,6 +63,10 @@ module Committee
63
63
 
64
64
  private
65
65
 
66
+ def build_options(options)
67
+ Options::ResponseValidation.from(options)
68
+ end
69
+
66
70
  def validate(request, status, headers, response, streaming_content_parser = nil)
67
71
  v = build_schema_validator(request)
68
72
  if v.link_exist? && self.class.validate?(status, validate_success_only)
@@ -5,6 +5,7 @@ module Committee
5
5
  end
6
6
  end
7
7
 
8
+ require_relative "middleware/options"
8
9
  require_relative "middleware/base"
9
10
  require_relative "middleware/request_validation"
10
11
  require_relative "middleware/response_validation"
@@ -73,9 +73,7 @@ module Committee
73
73
  {}
74
74
 
75
75
  else
76
- raise(%{At "#{link.method} #{link.href}" "#{schema.pointer}": no } +
77
- %{"example" attribute and "null" } +
78
- %{is not allowed; don't know how to generate property.})
76
+ raise(%{At "#{link.method} #{link.href}" "#{schema.pointer}": no } + %{"example" attribute and "null" } + %{is not allowed; don't know how to generate property.})
79
77
  end
80
78
  end
81
79
 
@@ -53,7 +53,7 @@ module Committee
53
53
  end
54
54
 
55
55
  begin
56
- if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
56
+ if validate?(status)
57
57
  if @link.is_a?(Drivers::OpenAPI2::Link)
58
58
  raise InvalidResponse, "Invalid response.#{@link.href} status code #{status} definition does not exist" if @validators[status].nil?
59
59
  if !@validators[status].validate(data)
@@ -73,6 +73,19 @@ module Committee
73
73
  end
74
74
  end
75
75
 
76
+ def validate?(status)
77
+ case status
78
+ when 204
79
+ false
80
+ when 200..299
81
+ true
82
+ when 304
83
+ false
84
+ else
85
+ !validate_success_only
86
+ end
87
+ end
88
+
76
89
  private
77
90
 
78
91
  def response_media_type(response)
@@ -6,7 +6,7 @@ module Committee
6
6
  class Router
7
7
  def initialize(schema, validator_option)
8
8
  @prefix = validator_option.prefix
9
- @prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
9
+ @prefix_regexp = ::Committee::SchemaValidator.build_prefix_regexp(@prefix)
10
10
  @schema = schema
11
11
 
12
12
  @validator_option = validator_option
@@ -65,13 +65,7 @@ module Committee
65
65
  end
66
66
 
67
67
  def request_unpack(request)
68
- unpacker = Committee::RequestUnpacker.new(
69
- allow_form_params: validator_option.allow_form_params,
70
- allow_get_body: validator_option.allow_get_body,
71
- allow_query_params: validator_option.allow_query_params,
72
- allow_non_get_query_params: validator_option.allow_non_get_query_params,
73
- optimistic_json: validator_option.optimistic_json,
74
- )
68
+ unpacker = Committee::RequestUnpacker.new(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,)
75
69
 
76
70
  request.env[validator_option.headers_key] = unpacker.unpack_headers(request)
77
71
 
@@ -109,12 +103,7 @@ module Committee
109
103
  def parameter_coerce!(request, link, coerce_key)
110
104
  return unless link_exist?
111
105
 
112
- Committee::SchemaValidator::HyperSchema::ParameterCoercer.
113
- new(request.env[coerce_key],
114
- link.schema,
115
- coerce_date_times: validator_option.coerce_date_times,
116
- coerce_recursive: validator_option.coerce_recursive).
117
- call!
106
+ Committee::SchemaValidator::HyperSchema::ParameterCoercer.new(request.env[coerce_key], link.schema, coerce_date_times: validator_option.coerce_date_times, coerce_recursive: validator_option.coerce_recursive).call!
118
107
  end
119
108
  end
120
109
  end
@@ -23,9 +23,10 @@ module Committee
23
23
 
24
24
  def coerce_path_parameter(validator_option)
25
25
  options = build_openapi_parser_path_option(validator_option)
26
+ validated_path_params = request_operation.validate_path_params(options)
26
27
  return {} unless options.coerce_value
27
28
 
28
- request_operation.validate_path_params(options)
29
+ validated_path_params
29
30
  rescue OpenAPIParser::OpenAPIError => e
30
31
  raise Committee::InvalidRequest.new(e.message, original_error: e)
31
32
  end
@@ -34,9 +35,7 @@ module Committee
34
35
  def validate_response_params(status_code, headers, response_data, strict, check_header, validator_options: {})
35
36
  response_body = OpenAPIParser::RequestOperation::ValidatableResponseBody.new(status_code, response_data, headers)
36
37
 
37
- return request_operation.validate_response_body(
38
- response_body,
39
- response_validate_options(strict, check_header, validator_options: validator_options))
38
+ return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header, validator_options: validator_options))
40
39
  rescue OpenAPIParser::OpenAPIError => e
41
40
  raise Committee::InvalidResponse.new(e.message, original_error: e)
42
41
  end
@@ -72,12 +71,18 @@ module Committee
72
71
  request_operation.operation_object&.request_body&.content&.keys || []
73
72
  end
74
73
 
75
- private
76
-
74
+ # Expose request_operation for parameter deserialization
75
+ # @return [OpenAPIParser::RequestOperation]
77
76
  attr_reader :request_operation
78
77
 
79
- # @!attribute [r] request_operation
80
- # @return [OpenAPIParser::RequestOperation]
78
+ # @return [Array<String>] names of query parameters defined in the schema
79
+ def query_parameter_names
80
+ return [] unless request_operation.operation_object&.parameters
81
+
82
+ request_operation.operation_object.parameters.select { |p| p.in == 'query' }.map(&:name)
83
+ end
84
+
85
+ private
81
86
 
82
87
  # @return [OpenAPIParser::SchemaValidator::Options]
83
88
  def build_openapi_parser_body_option(validator_option)
@@ -86,6 +91,11 @@ module Committee
86
91
 
87
92
  # @return [OpenAPIParser::SchemaValidator::Options]
88
93
  def build_openapi_parser_path_option(validator_option)
94
+ build_openapi_parser_option(validator_option, validator_option.coerce_path_params)
95
+ end
96
+
97
+ # @return [OpenAPIParser::SchemaValidator::Options]
98
+ def build_openapi_parser_request_parameter_option(validator_option)
89
99
  build_openapi_parser_option(validator_option, validator_option.coerce_query_params)
90
100
  end
91
101
 
@@ -121,6 +131,9 @@ module Committee
121
131
  end
122
132
 
123
133
  def validate_path_and_query_params(path_params, query_params, headers, validator_option)
134
+ path_params ||= {}
135
+ query_params ||= {}
136
+
124
137
  # it's currently impossible to validate path params and query params separately
125
138
  # so we have to resort to this workaround
126
139
 
@@ -129,22 +142,39 @@ module Committee
129
142
 
130
143
  merged_params = query_params.merge(path_params)
131
144
 
132
- request_operation.validate_request_parameter(merged_params, headers, build_openapi_parser_path_option(validator_option))
145
+ request_operation.validate_request_parameter(merged_params, headers, build_openapi_parser_request_parameter_option(validator_option))
133
146
 
134
147
  merged_params.each do |k, v|
135
148
  path_params[k] = v if path_keys.include?(k)
136
149
  query_params[k] = v if query_keys.include?(k)
137
150
  end
151
+
152
+ validate_no_unknown_query_params(query_params) if validator_option.strict_query_params
153
+ end
154
+
155
+ def validate_no_unknown_query_params(query_params)
156
+ return if query_params.nil? || query_params.empty?
157
+
158
+ defined_params = query_parameter_names
159
+ unknown_params = query_params.keys - defined_params
160
+
161
+ return if unknown_params.empty?
162
+
163
+ raise Committee::InvalidRequest.new("Unknown query parameter(s): #{unknown_params.join(', ')}")
138
164
  end
139
165
 
140
166
  def response_validate_options(strict, check_header, validator_options: {})
141
167
  options = { strict: strict, validate_header: check_header }
142
168
 
143
- if OpenAPIParser::SchemaValidator::ResponseValidateOptions.method_defined?(:validator_options)
144
- ::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options, **validator_options)
145
- else
146
- ::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options)
169
+ if validator_options[:coerce_value]
170
+ options[:coerce_value] = validator_options[:coerce_value]
147
171
  end
172
+
173
+ if validator_options[:allow_empty_date_and_datetime]
174
+ options[:allow_empty_date_and_datetime] = validator_options[:allow_empty_date_and_datetime]
175
+ end
176
+
177
+ ::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options)
148
178
  end
149
179
  end
150
180
  end