committee 3.3.0 → 5.0.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/committee/drivers/open_api_2/driver.rb +1 -2
  3. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +1 -1
  4. data/lib/committee/drivers.rb +22 -10
  5. data/lib/committee/errors.rb +12 -0
  6. data/lib/committee/middleware/base.rb +5 -4
  7. data/lib/committee/middleware/request_validation.rb +4 -18
  8. data/lib/committee/middleware/response_validation.rb +15 -16
  9. data/lib/committee/request_unpacker.rb +46 -60
  10. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +8 -2
  11. data/lib/committee/schema_validator/hyper_schema.rb +41 -27
  12. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +44 -37
  13. data/lib/committee/schema_validator/open_api_3/request_validator.rb +11 -2
  14. data/lib/committee/schema_validator/open_api_3/router.rb +3 -1
  15. data/lib/committee/schema_validator/open_api_3.rb +52 -26
  16. data/lib/committee/schema_validator/option.rb +14 -3
  17. data/lib/committee/schema_validator.rb +1 -1
  18. data/lib/committee/test/methods.rb +27 -16
  19. data/lib/committee/test/schema_coverage.rb +101 -0
  20. data/lib/committee/utils.rb +28 -0
  21. data/lib/committee/validation_error.rb +3 -2
  22. data/lib/committee/version.rb +5 -0
  23. data/lib/committee.rb +11 -4
  24. data/test/bin/committee_stub_test.rb +5 -1
  25. data/test/committee_test.rb +29 -3
  26. data/test/drivers/open_api_3/driver_test.rb +1 -1
  27. data/test/drivers_test.rb +20 -7
  28. data/test/middleware/base_test.rb +9 -10
  29. data/test/middleware/request_validation_open_api_3_test.rb +175 -18
  30. data/test/middleware/request_validation_test.rb +20 -28
  31. data/test/middleware/response_validation_open_api_3_test.rb +96 -7
  32. data/test/middleware/response_validation_test.rb +21 -26
  33. data/test/middleware/stub_test.rb +4 -0
  34. data/test/request_unpacker_test.rb +51 -110
  35. data/test/schema_validator/hyper_schema/response_validator_test.rb +10 -0
  36. data/test/schema_validator/hyper_schema/router_test.rb +4 -0
  37. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +1 -1
  38. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +72 -20
  39. data/test/schema_validator/open_api_3/request_validator_test.rb +27 -0
  40. data/test/schema_validator/open_api_3/response_validator_test.rb +26 -5
  41. data/test/test/methods_new_version_test.rb +17 -5
  42. data/test/test/methods_test.rb +155 -31
  43. data/test/test/schema_coverage_test.rb +216 -0
  44. data/test/test_helper.rb +34 -4
  45. metadata +47 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c35304cf9026ae20062ea312cdca6b43af486c4c2e5e7c952b35bbad811ce4e
4
- data.tar.gz: d9a96cd2c6c15b9de26fb95ef0b4d59216c999f8589329d6ee6b21b41e2511d9
3
+ metadata.gz: 284c8c1198255435e959dcee2b92cb25c3d80dd5fe6d4622b8077a69525712ed
4
+ data.tar.gz: b306ec0ad5e628d9cc7b6382781c4b710c99bedb127a7000edadf9d0e0b8fa84
5
5
  SHA512:
6
- metadata.gz: 42ffc889dcc301c9de500417390454f4723bf049f0336b9de92b94fcaa2d67f53a7f5c4c9ad5a3a56bd2e9fff70d06ebe7c7279729c616b45ab33625c2a33572
7
- data.tar.gz: d3c7ca273497b2eb4444fcdb6eb43f49fed866335d1a627f806586f6755557b8a956b43eb5c9448b7cf19176b9e2a4705c78bf6045dde2e98b19fd55211e0070
6
+ metadata.gz: 687d9a47d2a17786313bde939339d464a8d775795458b09ad7d7784bfa337f848a9ffabe240e9f39173c4466589c026f64a8eede768e7abfff4b01b2672339f5
7
+ data.tar.gz: 4ee7781341de9ebafae28d01d98c0f72d54067c425f3ee10b221c4eb88da85e79b36853bbda4818b20348177f8d975103cc5be694ff7e8252a6634a856853b7c
@@ -59,7 +59,7 @@ module Committee
59
59
  schema.base_path = data['basePath'] || ''
60
60
 
61
61
  # Arbitrarily choose the first media type found in these arrays. This
62
- # appraoch could probably stand to be improved, but at least users will
62
+ # approach could probably stand to be improved, but at least users will
63
63
  # for now have the option of turning media type validation off if they so
64
64
  # choose.
65
65
  schema.consumes = data['consumes'].first
@@ -156,7 +156,6 @@ module Committee
156
156
 
157
157
  methods.each do |method, link_data|
158
158
  method = method.upcase
159
-
160
159
  link = Link.new
161
160
  link.enc_type = schema.consumes
162
161
  link.href = href
@@ -62,7 +62,7 @@ module Committee
62
62
  # And same idea: despite parameters not being schemas, the items
63
63
  # key (if preset) is actually a schema that defines each item of an
64
64
  # array type, so we can just reflect that directly onto our
65
- # artifical schema.
65
+ # artificial schema.
66
66
  if param_data["type"] == "array" && param_data["items"]
67
67
  param_schema.items = param_data["items"]
68
68
  end
@@ -20,26 +20,27 @@ module Committee
20
20
  # load and build drive from JSON file
21
21
  # @param [String] schema_path
22
22
  # @return [Committee::Driver]
23
- def self.load_from_json(schema_path)
24
- load_from_data(JSON.parse(File.read(schema_path)))
23
+ def self.load_from_json(schema_path, parser_options: {})
24
+ load_from_data(JSON.parse(File.read(schema_path)), schema_path, parser_options: parser_options)
25
25
  end
26
26
 
27
27
  # load and build drive from YAML file
28
28
  # @param [String] schema_path
29
29
  # @return [Committee::Driver]
30
- def self.load_from_yaml(schema_path)
31
- load_from_data(YAML.load_file(schema_path))
30
+ def self.load_from_yaml(schema_path, parser_options: {})
31
+ data = YAML.respond_to?(:unsafe_load_file) ? YAML.unsafe_load_file(schema_path) : YAML.load_file(schema_path)
32
+ load_from_data(data, schema_path, parser_options: parser_options)
32
33
  end
33
34
 
34
35
  # load and build drive from file
35
36
  # @param [String] schema_path
36
37
  # @return [Committee::Driver]
37
- def self.load_from_file(schema_path)
38
+ def self.load_from_file(schema_path, parser_options: {})
38
39
  case File.extname(schema_path)
39
40
  when '.json'
40
- load_from_json(schema_path)
41
+ load_from_json(schema_path, parser_options: parser_options)
41
42
  when '.yaml', '.yml'
42
- load_from_yaml(schema_path)
43
+ load_from_yaml(schema_path, parser_options: parser_options)
43
44
  else
44
45
  raise "Committee only supports the following file extensions: '.json', '.yaml', '.yml'"
45
46
  end
@@ -48,10 +49,20 @@ module Committee
48
49
  # load and build drive from Hash object
49
50
  # @param [Hash] hash
50
51
  # @return [Committee::Driver]
51
- def self.load_from_data(hash)
52
+ def self.load_from_data(hash, schema_path = nil, parser_options: {})
52
53
  if hash['openapi']&.start_with?('3.0.')
53
- parser = OpenAPIParser.parse(hash)
54
- return Committee::Drivers::OpenAPI3::Driver.new.parse(parser)
54
+ # From the next major version, we want to ensure `{ strict_reference_validation: true }`
55
+ # as a parser option here, but since it may break existing implementations, just warn
56
+ # if it is not explicitly set. See: https://github.com/interagent/committee/issues/343#issuecomment-997400329
57
+ opts = parser_options.dup
58
+
59
+ Committee.warn_deprecated_until_6(!opts.key?(:strict_reference_validation), 'openapi_parser will default to strict reference validation ' +
60
+ 'from next version. Pass config `strict_reference_validation: true` (or false, if you must) ' +
61
+ 'to quiet this warning.')
62
+ opts[:strict_reference_validation] ||= false
63
+
64
+ openapi = OpenAPIParser.parse_with_filepath(hash, schema_path, opts)
65
+ return Committee::Drivers::OpenAPI3::Driver.new.parse(openapi)
55
66
  end
56
67
 
57
68
  driver = if hash['swagger'] == '2.0'
@@ -60,6 +71,7 @@ module Committee
60
71
  Committee::Drivers::HyperSchema::Driver.new
61
72
  end
62
73
 
74
+ # TODO: in the future, pass `opts` here and allow optionality in other drivers?
63
75
  driver.parse(hash)
64
76
  end
65
77
  end
@@ -8,9 +8,21 @@ module Committee
8
8
  end
9
9
 
10
10
  class InvalidRequest < Error
11
+ attr_reader :original_error
12
+
13
+ def initialize(error_message=nil, original_error: nil)
14
+ @original_error = original_error
15
+ super(error_message)
16
+ end
11
17
  end
12
18
 
13
19
  class InvalidResponse < Error
20
+ attr_reader :original_error
21
+
22
+ def initialize(error_message=nil, original_error: nil)
23
+ @original_error = original_error
24
+ super(error_message)
25
+ end
14
26
  end
15
27
 
16
28
  class NotFound < Error
@@ -30,11 +30,12 @@ module Committee
30
30
  class << self
31
31
  def get_schema(options)
32
32
  schema = options[:schema]
33
- unless schema
34
- schema = Committee::Drivers::load_from_file(options[:schema_path]) if options[:schema_path]
35
-
36
- raise(ArgumentError, "Committee: need option `schema` or `schema_path`") unless schema
33
+ if !schema && options[:schema_path]
34
+ # In the future, we could have `parser_options` as an exposed config?
35
+ parser_options = options.key?(:strict_reference_validation) ? { strict_reference_validation: options[:strict_reference_validation] } : {}
36
+ schema = Committee::Drivers::load_from_file(options[:schema_path], parser_options: parser_options)
37
37
  end
38
+ raise(ArgumentError, "Committee: need option `schema` or `schema_path`") unless schema
38
39
 
39
40
  # Expect the type we want by now. If we don't have it, the user passed
40
41
  # something else non-standard in.
@@ -7,9 +7,6 @@ module Committee
7
7
  super
8
8
 
9
9
  @strict = options[:strict]
10
-
11
- # deprecated
12
- @allow_extra = options[:allow_extra]
13
10
  end
14
11
 
15
12
  def handle(request)
@@ -21,14 +18,14 @@ module Committee
21
18
  rescue Committee::BadRequest, Committee::InvalidRequest
22
19
  handle_exception($!, request.env)
23
20
  raise if @raise
24
- return @error_class.new(400, :bad_request, $!.message).render unless @ignore_error
21
+ return @error_class.new(400, :bad_request, $!.message, request).render unless @ignore_error
25
22
  rescue Committee::NotFound => e
26
23
  raise if @raise
27
- return @error_class.new(404, :not_found, e.message).render unless @ignore_error
24
+ return @error_class.new(404, :not_found, e.message, request).render unless @ignore_error
28
25
  rescue JSON::ParserError
29
26
  handle_exception($!, request.env)
30
27
  raise Committee::InvalidRequest if @raise
31
- return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.").render unless @ignore_error
28
+ return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.", request).render unless @ignore_error
32
29
  end
33
30
 
34
31
  @app.call(request.env)
@@ -37,18 +34,7 @@ module Committee
37
34
  private
38
35
 
39
36
  def handle_exception(e, env)
40
- return unless @error_handler
41
-
42
- if @error_handler.arity > 1
43
- @error_handler.call(e, env)
44
- else
45
- warn <<-MESSAGE
46
- [DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to
47
- `error_handler.call(exception, request.env)` in next major version.
48
- MESSAGE
49
-
50
- @error_handler.call(e)
51
- end
37
+ @error_handler.call(e, env) if @error_handler
52
38
  end
53
39
  end
54
40
  end
@@ -7,15 +7,16 @@ module Committee
7
7
 
8
8
  def initialize(app, options = {})
9
9
  super
10
+ @strict = options[:strict]
10
11
  @validate_success_only = @schema.validator_option.validate_success_only
11
12
  end
12
13
 
13
14
  def handle(request)
14
- begin
15
- status, headers, response = @app.call(request.env)
15
+ status, headers, response = @app.call(request.env)
16
16
 
17
+ begin
17
18
  v = build_schema_validator(request)
18
- v.response_validate(status, headers, response) if v.link_exist? && self.class.validate?(status, validate_success_only)
19
+ v.response_validate(status, headers, response, @strict) if v.link_exist? && self.class.validate?(status, validate_success_only)
19
20
 
20
21
  rescue Committee::InvalidResponse
21
22
  handle_exception($!, request.env)
@@ -34,25 +35,23 @@ module Committee
34
35
 
35
36
  class << self
36
37
  def validate?(status, validate_success_only)
37
- status != 204 && (!validate_success_only || (200...300).include?(status))
38
+ case status
39
+ when 204
40
+ false
41
+ when 200..299
42
+ true
43
+ when 304
44
+ false
45
+ else
46
+ !validate_success_only
47
+ end
38
48
  end
39
49
  end
40
50
 
41
51
  private
42
52
 
43
53
  def handle_exception(e, env)
44
- return unless @error_handler
45
-
46
- if @error_handler.arity > 1
47
- @error_handler.call(e, env)
48
- else
49
- warn <<-MESSAGE
50
- [DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to
51
- `error_handler.call(exception, request.env)` in next major version.
52
- MESSAGE
53
-
54
- @error_handler.call(e)
55
- end
54
+ @error_handler.call(e, env) if @error_handler
56
55
  end
57
56
  end
58
57
  end
@@ -2,84 +2,82 @@
2
2
 
3
3
  module Committee
4
4
  class RequestUnpacker
5
- def initialize(request, options={})
6
- @request = request
5
+ class << self
6
+ # Enable string or symbol key access to the nested params hash.
7
+ #
8
+ # (Copied from Sinatra)
9
+ def indifferent_params(object)
10
+ case object
11
+ when Hash
12
+ new_hash = Committee::Utils.indifferent_hash
13
+ object.each { |key, value| new_hash[key] = indifferent_params(value) }
14
+ new_hash
15
+ when Array
16
+ object.map { |item| indifferent_params(item) }
17
+ else
18
+ object
19
+ end
20
+ end
21
+ end
7
22
 
23
+ def initialize(options={})
8
24
  @allow_form_params = options[:allow_form_params]
9
25
  @allow_get_body = options[:allow_get_body]
10
26
  @allow_query_params = options[:allow_query_params]
11
- @coerce_form_params = options[:coerce_form_params]
12
27
  @optimistic_json = options[:optimistic_json]
13
- @schema_validator = options[:schema_validator]
14
28
  end
15
29
 
16
- def call
30
+ # return params and is_form_params
31
+ def unpack_request_params(request)
17
32
  # if Content-Type is empty or JSON, and there was a request body, try to
18
33
  # interpret it as JSON
19
- params = if !@request.media_type || @request.media_type =~ %r{application/.*json}
20
- parse_json
34
+ params = if !request.media_type || request.media_type =~ %r{application/(?:.*\+)?json}
35
+ parse_json(request)
21
36
  elsif @optimistic_json
22
37
  begin
23
- parse_json
38
+ parse_json(request)
24
39
  rescue JSON::ParserError
25
40
  nil
26
41
  end
27
42
  end
28
43
 
29
- params = if params
30
- params
31
- elsif @allow_form_params && %w[application/x-www-form-urlencoded multipart/form-data].include?(@request.media_type)
44
+ return [params, false] if params
45
+
46
+ if @allow_form_params && %w[application/x-www-form-urlencoded multipart/form-data].include?(request.media_type)
32
47
  # Actually, POST means anything in the request body, could be from
33
48
  # PUT or PATCH too. Silly Rack.
34
- p = @request.POST
35
-
36
- @schema_validator.coerce_form_params(p) if @coerce_form_params
37
-
38
- p
39
- else
40
- {}
49
+ return [request.POST, true] if request.POST
41
50
  end
42
51
 
43
- if @allow_query_params
44
- [indifferent_params(@request.GET).merge(params), headers]
45
- else
46
- [params, headers]
47
- end
52
+ [{}, false]
48
53
  end
49
54
 
50
- private
51
-
52
- # Creates a Hash with indifferent access.
53
- #
54
- # (Copied from Sinatra)
55
- def indifferent_hash
56
- Hash.new { |hash,key| hash[key.to_s] if Symbol === key }
55
+ def unpack_query_params(request)
56
+ @allow_query_params ? self.class.indifferent_params(request.GET) : {}
57
57
  end
58
58
 
59
- # Enable string or symbol key access to the nested params hash.
60
- #
61
- # (Copied from Sinatra)
62
- def indifferent_params(object)
63
- case object
64
- when Hash
65
- new_hash = indifferent_hash
66
- object.each { |key, value| new_hash[key] = indifferent_params(value) }
67
- new_hash
68
- when Array
69
- object.map { |item| indifferent_params(item) }
70
- else
71
- object
59
+ def unpack_headers(request)
60
+ env = request.env
61
+ base = env.keys.grep(/HTTP_/).inject({}) do |headers, key|
62
+ headerized_key = key.gsub(/^HTTP_/, '').gsub(/_/, '-')
63
+ headers[headerized_key] = env[key]
64
+ headers
72
65
  end
66
+
67
+ base['Content-Type'] = env['CONTENT_TYPE'] if env['CONTENT_TYPE']
68
+ base
73
69
  end
74
70
 
75
- def parse_json
76
- return nil if @request.request_method == "GET" && !@allow_get_body
71
+ private
72
+
73
+ def parse_json(request)
74
+ return nil if request.request_method == "GET" && !@allow_get_body
77
75
 
78
- body = @request.body.read
76
+ body = request.body.read
79
77
  # if request body is empty, we just have empty params
80
78
  return nil if body.length == 0
81
79
 
82
- @request.body.rewind
80
+ request.body.rewind
83
81
  hash = JSON.parse(body)
84
82
  # We want a hash specifically. '42', 42, and [42] will all be
85
83
  # decoded properly, but we can't use them here.
@@ -87,19 +85,7 @@ module Committee
87
85
  raise BadRequest,
88
86
  "Invalid JSON input. Require object with parameters as keys."
89
87
  end
90
- indifferent_params(hash)
91
- end
92
-
93
- def headers
94
- env = @request.env
95
- base = env.keys.grep(/HTTP_/).inject({}) do |headers, key|
96
- headerized_key = key.gsub(/^HTTP_/, '').gsub(/_/, '-')
97
- headers[headerized_key] = env[key]
98
- headers
99
- end
100
-
101
- base['Content-Type'] = env['CONTENT_TYPE'] if env['CONTENT_TYPE']
102
- base
88
+ self.class.indifferent_params(hash)
103
89
  end
104
90
  end
105
91
  end
@@ -14,7 +14,7 @@ module Committee
14
14
  end
15
15
 
16
16
  def call(status, headers, data)
17
- unless status == 204 # 204 No Content
17
+ unless [204, 304].include?(status) # 204 No Content or 304 Not Modified
18
18
  response = Rack::Response.new(data, status, headers)
19
19
  check_content_type!(response)
20
20
  end
@@ -48,9 +48,15 @@ module Committee
48
48
  private
49
49
 
50
50
  def response_media_type(response)
51
- response.content_type.to_s.split(";").first.to_s
51
+ if response.respond_to?(:media_type)
52
+ response.media_type.to_s
53
+ else
54
+ # for rack compatibility. In rack v 1.5.0, Rack::Response doesn't have media_type
55
+ response.content_type.to_s.split(";").first.to_s
56
+ end
52
57
  end
53
58
 
59
+
54
60
  def check_content_type!(response)
55
61
  if @link.media_type
56
62
  unless Rack::Mime.match?(response_media_type(response), @link.media_type)
@@ -11,18 +11,8 @@ module Committee
11
11
  end
12
12
 
13
13
  def request_validate(request)
14
- # Attempts to coerce parameters that appear in a link's URL to Ruby
15
- # types that can be validated with a schema.
16
- param_matches_hash = validator_option.coerce_path_params ? coerce_path_params : {}
17
-
18
- # Attempts to coerce parameters that appear in a query string to Ruby
19
- # types that can be validated with a schema.
20
- coerce_query_params(request) if validator_option.coerce_query_params
21
-
22
14
  request_unpack(request)
23
15
 
24
- request.env[validator_option.params_key].merge!(param_matches_hash) if param_matches_hash
25
-
26
16
  request_schema_validation(request)
27
17
  parameter_coerce!(request, link, validator_option.params_key)
28
18
  parameter_coerce!(request, link, "rack.request.query_hash") if link_exist? && !request.GET.nil? && !link.schema.nil?
@@ -35,7 +25,14 @@ module Committee
35
25
  response.each do |chunk|
36
26
  full_body << chunk
37
27
  end
38
- data = full_body.empty? ? {} : JSON.parse(full_body)
28
+
29
+ data = {}
30
+ unless full_body.empty?
31
+ parse_to_json = !validator_option.parse_response_by_content_type ||
32
+ headers.fetch('Content-Type', nil)&.start_with?('application/json')
33
+ data = JSON.parse(full_body) if parse_to_json
34
+ end
35
+
39
36
  Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: validator_option.validate_success_only).call(status, headers, data)
40
37
  end
41
38
 
@@ -43,16 +40,10 @@ module Committee
43
40
  !link.nil?
44
41
  end
45
42
 
46
- def coerce_form_params(parameter)
47
- return unless link_exist?
48
- return unless link.schema
49
- Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(parameter, link.schema).call!
50
- end
51
-
52
43
  private
53
44
 
54
45
  def coerce_path_params
55
- return unless link_exist?
46
+ return {} unless link_exist?
56
47
 
57
48
  Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(param_matches, link.schema, coerce_recursive: validator_option.coerce_recursive).call!
58
49
  param_matches
@@ -66,15 +57,38 @@ module Committee
66
57
  end
67
58
 
68
59
  def request_unpack(request)
69
- request.env[validator_option.params_key], request.env[validator_option.headers_key] = Committee::RequestUnpacker.new(
70
- request,
71
- allow_form_params: validator_option.allow_form_params,
72
- allow_get_body: validator_option.allow_get_body,
73
- allow_query_params: validator_option.allow_query_params,
74
- coerce_form_params: validator_option.coerce_form_params,
75
- optimistic_json: validator_option.optimistic_json,
76
- schema_validator: self
77
- ).call
60
+ unpacker = Committee::RequestUnpacker.new(
61
+ allow_form_params: validator_option.allow_form_params,
62
+ allow_get_body: validator_option.allow_get_body,
63
+ allow_query_params: validator_option.allow_query_params,
64
+ optimistic_json: validator_option.optimistic_json,
65
+ )
66
+
67
+ request.env[validator_option.headers_key] = unpacker.unpack_headers(request)
68
+
69
+ # Attempts to coerce parameters that appear in a link's URL to Ruby
70
+ # types that can be validated with a schema.
71
+ param_matches_hash = validator_option.coerce_path_params ? coerce_path_params : {}
72
+
73
+ # Attempts to coerce parameters that appear in a query string to Ruby
74
+ # types that can be validated with a schema.
75
+ coerce_query_params(request) if validator_option.coerce_query_params
76
+
77
+ query_param = unpacker.unpack_query_params(request)
78
+ request_param, is_form_params = unpacker.unpack_request_params(request)
79
+ coerce_form_params(request_param) if validator_option.coerce_form_params && is_form_params
80
+ request.env[validator_option.request_body_hash_key] = request_param
81
+
82
+ request.env[validator_option.params_key] = Committee::Utils.indifferent_hash
83
+ request.env[validator_option.params_key].merge!(Committee::Utils.deep_copy(query_param))
84
+ request.env[validator_option.params_key].merge!(Committee::Utils.deep_copy(request_param))
85
+ request.env[validator_option.params_key].merge!(Committee::Utils.deep_copy(param_matches_hash))
86
+ end
87
+
88
+ def coerce_form_params(parameter)
89
+ return unless link_exist?
90
+ return unless link.schema
91
+ Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(parameter, link.schema).call!
78
92
  end
79
93
 
80
94
  def request_schema_validation(request)
@@ -17,13 +17,17 @@ module Committee
17
17
  request_operation.original_path
18
18
  end
19
19
 
20
+ def http_method
21
+ request_operation.http_method
22
+ end
23
+
20
24
  def coerce_path_parameter(validator_option)
21
25
  options = build_openapi_parser_path_option(validator_option)
22
26
  return {} unless options.coerce_value
23
27
 
24
28
  request_operation.validate_path_params(options)
25
29
  rescue OpenAPIParser::OpenAPIError => e
26
- raise Committee::InvalidRequest.new(e.message)
30
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
27
31
  end
28
32
 
29
33
  # @param [Boolean] strict when not content_type or status code definition, raise error
@@ -32,21 +36,15 @@ module Committee
32
36
 
33
37
  return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header))
34
38
  rescue OpenAPIParser::OpenAPIError => e
35
- raise Committee::InvalidResponse.new(e.message)
39
+ raise Committee::InvalidResponse.new(e.message, original_error: e)
36
40
  end
37
41
 
38
- def validate_request_params(params, headers, validator_option)
42
+ def validate_request_params(path_params, query_params, body_params, headers, validator_option)
39
43
  ret, err = case request_operation.http_method
40
- when 'get'
41
- validate_get_request_params(params, headers, validator_option)
42
- when 'post'
43
- validate_post_request_params(params, headers, validator_option)
44
- when 'put'
45
- validate_post_request_params(params, headers, validator_option)
46
- when 'patch'
47
- validate_post_request_params(params, headers, validator_option)
48
- when 'delete'
49
- validate_get_request_params(params, headers, validator_option)
44
+ when 'get', 'delete', 'head'
45
+ validate_get_request_params(path_params, query_params, headers, validator_option)
46
+ when 'post', 'put', 'patch', 'options'
47
+ validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
50
48
  else
51
49
  raise "Committee OpenAPI3 not support #{request_operation.http_method} method"
52
50
  end
@@ -54,6 +52,10 @@ module Committee
54
52
  ret
55
53
  end
56
54
 
55
+ def optional_body?
56
+ !request_operation.operation_object&.request_body&.required
57
+ end
58
+
57
59
  def valid_request_content_type?(content_type)
58
60
  if (request_body = request_operation.operation_object&.request_body)
59
61
  !request_body.select_media_type(content_type).nil?
@@ -76,28 +78,17 @@ module Committee
76
78
  # @return [OpenAPIParser::RequestOperation]
77
79
 
78
80
  # @return [OpenAPIParser::SchemaValidator::Options]
79
- def build_openapi_parser_path_option(validator_option)
80
- coerce_value = validator_option.coerce_path_params
81
- datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
82
- validate_header = validator_option.check_header
83
- OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,
84
- datetime_coerce_class: datetime_coerce_class,
85
- validate_header: validate_header)
81
+ def build_openapi_parser_body_option(validator_option)
82
+ build_openapi_parser_option(validator_option, validator_option.coerce_form_params)
86
83
  end
87
84
 
88
85
  # @return [OpenAPIParser::SchemaValidator::Options]
89
- def build_openapi_parser_post_option(validator_option)
90
- coerce_value = validator_option.coerce_form_params
91
- datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
92
- validate_header = validator_option.check_header
93
- OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,
94
- datetime_coerce_class: datetime_coerce_class,
95
- validate_header: validate_header)
86
+ def build_openapi_parser_path_option(validator_option)
87
+ build_openapi_parser_option(validator_option, validator_option.coerce_query_params)
96
88
  end
97
89
 
98
90
  # @return [OpenAPIParser::SchemaValidator::Options]
99
- def build_openapi_parser_get_option(validator_option)
100
- coerce_value = validator_option.coerce_query_params
91
+ def build_openapi_parser_option(validator_option, coerce_value)
101
92
  datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
102
93
  validate_header = validator_option.check_header
103
94
  OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,
@@ -105,22 +96,38 @@ module Committee
105
96
  validate_header: validate_header)
106
97
  end
107
98
 
108
- def validate_get_request_params(params, headers, validator_option)
99
+ def validate_get_request_params(path_params, query_params, headers, validator_option)
109
100
  # bad performance because when we coerce value, same check
110
- request_operation.validate_request_parameter(params, headers, build_openapi_parser_get_option(validator_option))
101
+ validate_path_and_query_params(path_params, query_params, headers, validator_option)
111
102
  rescue OpenAPIParser::OpenAPIError => e
112
- raise Committee::InvalidRequest.new(e.message)
103
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
113
104
  end
114
105
 
115
- def validate_post_request_params(params, headers, validator_option)
106
+ def validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
116
107
  content_type = headers['Content-Type'].to_s.split(";").first.to_s
117
108
 
118
109
  # bad performance because when we coerce value, same check
119
- schema_validator_options = build_openapi_parser_post_option(validator_option)
120
- request_operation.validate_request_parameter(params, headers, schema_validator_options)
121
- request_operation.validate_request_body(content_type, params, schema_validator_options)
110
+ validate_path_and_query_params(path_params, query_params, headers, validator_option)
111
+ request_operation.validate_request_body(content_type, body_params, build_openapi_parser_body_option(validator_option))
122
112
  rescue => e
123
- raise Committee::InvalidRequest.new(e.message)
113
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
114
+ end
115
+
116
+ def validate_path_and_query_params(path_params, query_params, headers, validator_option)
117
+ # it's currently impossible to validate path params and query params separately
118
+ # so we have to resort to this workaround
119
+
120
+ path_keys = path_params.keys.to_set
121
+ query_keys = query_params.keys.to_set
122
+
123
+ merged_params = query_params.merge(path_params)
124
+
125
+ request_operation.validate_request_parameter(merged_params, headers, build_openapi_parser_path_option(validator_option))
126
+
127
+ merged_params.each do |k, v|
128
+ path_params[k] = v if path_keys.include?(k)
129
+ query_params[k] = v if query_keys.include?(k)
130
+ end
124
131
  end
125
132
 
126
133
  def response_validate_options(strict, check_header)