committee 3.3.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)