committee 1.15.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +5 -5
  2. data/bin/committee-stub +11 -38
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +17 -0
  22. data/lib/committee/middleware/base.rb +46 -29
  23. data/lib/committee/middleware/request_validation.rb +31 -49
  24. data/lib/committee/middleware/response_validation.rb +48 -25
  25. data/lib/committee/middleware/stub.rb +62 -37
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +58 -50
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +68 -38
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +5 -2
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +31 -18
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +96 -7
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +423 -32
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +125 -23
  64. data/test/middleware/stub_test.rb +81 -20
  65. data/test/request_unpacker_test.rb +126 -52
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/{response_validator_test.rb → schema_validator/hyper_schema/response_validator_test.rb} +43 -6
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +334 -27
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +108 -1
  79. data/test/validation_error_test.rb +3 -1
  80. metadata +190 -27
  81. data/lib/committee/query_params_coercer.rb +0 -45
  82. data/lib/committee/request_validator.rb +0 -44
  83. data/lib/committee/response_generator.rb +0 -35
  84. data/lib/committee/response_validator.rb +0 -59
  85. data/lib/committee/router.rb +0 -62
  86. data/test/query_params_coercer_test.rb +0 -70
  87. data/test/request_validator_test.rb +0 -103
  88. data/test/response_generator_test.rb +0 -61
  89. data/test/router_test.rb +0 -38
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class Router
7
+ def initialize(schema, validator_option)
8
+ @prefix = validator_option.prefix
9
+ @prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
10
+ @schema = schema
11
+
12
+ @validator_option = validator_option
13
+ end
14
+
15
+ def includes?(path)
16
+ !@prefix || path =~ @prefix_regexp
17
+ end
18
+
19
+ def includes_request?(request)
20
+ includes?(request.path)
21
+ end
22
+
23
+ def find_link(method, path)
24
+ path = path.gsub(@prefix_regexp, "") if @prefix
25
+ link_with_matches = (@schema.routes[method] || []).map do |pattern, link|
26
+ if matches = pattern.match(path)
27
+ # prefer path which has fewer matches (eg. `/pets/dog` than `/pets/{uuid}` for path `/pets/dog` )
28
+ [matches.captures.size, link, Hash[matches.names.zip(matches.captures)]]
29
+ else
30
+ nil
31
+ end
32
+ end.compact.sort_by(&:first).first
33
+ link_with_matches.nil? ? nil : link_with_matches.slice(1, 2)
34
+ end
35
+
36
+ def find_request_link(request)
37
+ find_link(request.request_method, request.path_info)
38
+ end
39
+
40
+ def build_schema_validator(request)
41
+ Committee::SchemaValidator::HyperSchema.new(self, request, @validator_option)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ # StringParamsCoercer takes parameters that are specified over a medium that
7
+ # can only accept strings (for example in a URL path or in query parameters)
8
+ # and attempts to coerce them into known types based of a link's schema
9
+ # definition.
10
+ #
11
+ # Currently supported types: null, integer, number and boolean.
12
+ #
13
+ # +call+ returns a hash of all params which could be coerced - coercion
14
+ # errors are simply ignored and expected to be handled later by schema
15
+ # validation.
16
+ class StringParamsCoercer
17
+ def initialize(query_hash, schema, options = {})
18
+ @query_hash = query_hash
19
+ @schema = schema
20
+
21
+ @coerce_recursive = options.fetch(:coerce_recursive, false)
22
+ end
23
+
24
+ def call!
25
+ coerce_object!(@query_hash, @schema)
26
+ end
27
+
28
+ private
29
+
30
+ def coerce_object!(hash, schema)
31
+ return false unless schema.respond_to?(:properties)
32
+
33
+ is_coerced = false
34
+ schema.properties.each do |k, s|
35
+ original_val = hash[k]
36
+ unless original_val.nil?
37
+ new_value, is_changed = coerce_value!(original_val, s)
38
+ if is_changed
39
+ hash[k] = new_value
40
+ is_coerced = true
41
+ end
42
+ end
43
+ end
44
+
45
+ is_coerced
46
+ end
47
+
48
+ def coerce_value!(original_val, s)
49
+ unless original_val.nil?
50
+ s.type.each do |to_type|
51
+ case to_type
52
+ when "null"
53
+ return nil, true if original_val.empty?
54
+ when "integer"
55
+ begin
56
+ return Integer(original_val), true
57
+ rescue ArgumentError => e
58
+ raise e unless e.message =~ /invalid value for Integer/
59
+ end
60
+ when "number"
61
+ begin
62
+ return Float(original_val), true
63
+ rescue ArgumentError => e
64
+ raise e unless e.message =~ /invalid value for Float/
65
+ end
66
+ when "boolean"
67
+ if original_val == "true" || original_val == "1"
68
+ return true, true
69
+ end
70
+ if original_val == "false" || original_val == "0"
71
+ return false, true
72
+ end
73
+ when "array"
74
+ if @coerce_recursive && coerce_array_data!(original_val, s)
75
+ return original_val, true # change original value
76
+ end
77
+ when "object"
78
+ if @coerce_recursive && coerce_object!(original_val, s)
79
+ return original_val, true # change original value
80
+ end
81
+ end
82
+ end
83
+ end
84
+ return nil, false
85
+ end
86
+
87
+ def coerce_array_data!(original_val, schema)
88
+ return false unless schema.respond_to?(:items)
89
+ return false unless original_val.is_a?(Array)
90
+
91
+ is_coerced = false
92
+ original_val.each_with_index do |d, index|
93
+ new_value, is_changed = coerce_value!(d, schema.items)
94
+ if is_changed
95
+ original_val[index] = new_value
96
+ is_coerced = true
97
+ end
98
+ end
99
+
100
+ is_coerced
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ attr_reader :link, :param_matches, :validator_option
7
+
8
+ def initialize(router, request, validator_option)
9
+ @link, @param_matches = router.find_request_link(request)
10
+ @validator_option = validator_option
11
+ end
12
+
13
+ def request_validate(request)
14
+ request_unpack(request)
15
+
16
+ request_schema_validation(request)
17
+ parameter_coerce!(request, link, validator_option.params_key)
18
+ parameter_coerce!(request, link, "rack.request.query_hash") if link_exist? && !request.GET.nil? && !link.schema.nil?
19
+ end
20
+
21
+ def response_validate(status, headers, response, _test_method = false)
22
+ return unless link_exist?
23
+
24
+ full_body = +""
25
+ response.each do |chunk|
26
+ full_body << chunk
27
+ end
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
+
36
+ Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: validator_option.validate_success_only).call(status, headers, data)
37
+ end
38
+
39
+ def link_exist?
40
+ !link.nil?
41
+ end
42
+
43
+ private
44
+
45
+ def coerce_path_params
46
+ return {} unless link_exist?
47
+
48
+ Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(param_matches, link.schema, coerce_recursive: validator_option.coerce_recursive).call!
49
+ param_matches
50
+ end
51
+
52
+ def coerce_query_params(request)
53
+ return unless link_exist?
54
+ return if request.GET.nil? || link.schema.nil?
55
+
56
+ Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(request.GET, link.schema, coerce_recursive: validator_option.coerce_recursive).call!
57
+ end
58
+
59
+ def request_unpack(request)
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!
92
+ end
93
+
94
+ def request_schema_validation(request)
95
+ return unless link_exist?
96
+ validator = Committee::SchemaValidator::HyperSchema::RequestValidator.new(link, check_content_type: validator_option.check_content_type, check_header: validator_option.check_header)
97
+ validator.call(request, request.env[validator_option.params_key], request.env[validator_option.headers_key])
98
+ end
99
+
100
+ def parameter_coerce!(request, link, coerce_key)
101
+ return unless link_exist?
102
+
103
+ Committee::SchemaValidator::HyperSchema::ParameterCoercer.
104
+ new(request.env[coerce_key],
105
+ link.schema,
106
+ coerce_date_times: validator_option.coerce_date_times,
107
+ coerce_recursive: validator_option.coerce_recursive).
108
+ call!
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ require_relative "hyper_schema/request_validator"
115
+ require_relative "hyper_schema/response_generator"
116
+ require_relative "hyper_schema/response_validator"
117
+ require_relative "hyper_schema/router"
118
+ require_relative "hyper_schema/string_params_coercer"
119
+ require_relative "hyper_schema/parameter_coercer"
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class OperationWrapper
7
+ # # @param request_operation [OpenAPIParser::RequestOperation]
8
+ def initialize(request_operation)
9
+ @request_operation = request_operation
10
+ end
11
+
12
+ def path_params
13
+ request_operation.path_params
14
+ end
15
+
16
+ def original_path
17
+ request_operation.original_path
18
+ end
19
+
20
+ def http_method
21
+ request_operation.http_method
22
+ end
23
+
24
+ def coerce_path_parameter(validator_option)
25
+ options = build_openapi_parser_path_option(validator_option)
26
+ return {} unless options.coerce_value
27
+
28
+ request_operation.validate_path_params(options)
29
+ rescue OpenAPIParser::OpenAPIError => e
30
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
31
+ end
32
+
33
+ # @param [Boolean] strict when not content_type or status code definition, raise error
34
+ def validate_response_params(status_code, headers, response_data, strict, check_header)
35
+ response_body = OpenAPIParser::RequestOperation::ValidatableResponseBody.new(status_code, response_data, headers)
36
+
37
+ return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header))
38
+ rescue OpenAPIParser::OpenAPIError => e
39
+ raise Committee::InvalidResponse.new(e.message, original_error: e)
40
+ end
41
+
42
+ def validate_request_params(path_params, query_params, body_params, headers, validator_option)
43
+ ret, err = case request_operation.http_method
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)
48
+ else
49
+ raise "Committee OpenAPI3 not support #{request_operation.http_method} method"
50
+ end
51
+ raise err if err
52
+ ret
53
+ end
54
+
55
+ def optional_body?
56
+ !request_operation.operation_object&.request_body&.required
57
+ end
58
+
59
+ def valid_request_content_type?(content_type)
60
+ if (request_body = request_operation.operation_object&.request_body)
61
+ !request_body.select_media_type(content_type).nil?
62
+ else
63
+ # if not exist request body object, all content_type allow.
64
+ # because request body object required content field so when it exists there're content type definition.
65
+ true
66
+ end
67
+ end
68
+
69
+ def request_content_types
70
+ request_operation.operation_object&.request_body&.content&.keys || []
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :request_operation
76
+
77
+ # @!attribute [r] request_operation
78
+ # @return [OpenAPIParser::RequestOperation]
79
+
80
+ # @return [OpenAPIParser::SchemaValidator::Options]
81
+ def build_openapi_parser_body_option(validator_option)
82
+ build_openapi_parser_option(validator_option, validator_option.coerce_form_params)
83
+ end
84
+
85
+ # @return [OpenAPIParser::SchemaValidator::Options]
86
+ def build_openapi_parser_path_option(validator_option)
87
+ build_openapi_parser_option(validator_option, validator_option.coerce_query_params)
88
+ end
89
+
90
+ # @return [OpenAPIParser::SchemaValidator::Options]
91
+ def build_openapi_parser_option(validator_option, coerce_value)
92
+ datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
93
+ validate_header = validator_option.check_header
94
+ OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,
95
+ datetime_coerce_class: datetime_coerce_class,
96
+ validate_header: validate_header)
97
+ end
98
+
99
+ def validate_get_request_params(path_params, query_params, headers, validator_option)
100
+ # bad performance because when we coerce value, same check
101
+ validate_path_and_query_params(path_params, query_params, headers, validator_option)
102
+ rescue OpenAPIParser::OpenAPIError => e
103
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
104
+ end
105
+
106
+ def validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
107
+ content_type = headers['Content-Type'].to_s.split(";").first.to_s
108
+
109
+ # bad performance because when we coerce value, same check
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))
112
+ rescue => e
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
131
+ end
132
+
133
+ def response_validate_options(strict, check_header)
134
+ ::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(strict: strict, validate_header: check_header)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class RequestValidator
7
+ # @param [SchemaValidator::OpenAPI3::OperationWrapper] operation_object
8
+ # @param [Committee::SchemaValidator::Option] validator_option
9
+ def initialize(operation_object, validator_option:)
10
+ @operation_object = operation_object
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def call(request, path_params, query_params, body_params, headers)
15
+ content_type = ::Committee::SchemaValidator.request_media_type(request)
16
+ check_content_type(request, content_type) if @validator_option.check_content_type
17
+
18
+ @operation_object.validate_request_params(path_params, query_params, body_params, headers, @validator_option)
19
+ end
20
+
21
+ private
22
+
23
+ def check_content_type(request, content_type)
24
+ # support post, put, patch only
25
+ return true unless request.post? || request.put? || request.patch?
26
+ return true if @operation_object.valid_request_content_type?(content_type)
27
+ return true if @operation_object.optional_body? && empty_request?(request)
28
+
29
+ message = if valid_content_types.size > 1
30
+ types = valid_content_types.map {|x| %{"#{x}"} }.join(', ')
31
+ %{"Content-Type" request header must be set to any of the following: [#{types}].}
32
+ else
33
+ %{"Content-Type" request header must be set to "#{valid_content_types.first}".}
34
+ end
35
+ raise Committee::InvalidRequest, message
36
+ end
37
+
38
+ def valid_content_types
39
+ @operation_object&.request_content_types
40
+ end
41
+
42
+ def empty_request?(request)
43
+ return true if !request.body
44
+
45
+ data = request.body.read
46
+ request.body.rewind
47
+ data.empty?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class ResponseValidator
7
+ attr_reader :validate_success_only
8
+
9
+ # @param [Committee::SchemaValidator::Options] validator_option
10
+ # @param [Committee::SchemaValidator::OpenAPI3::OperationWrapper] operation_wrapper
11
+ def initialize(operation_wrapper, validator_option)
12
+ @operation_wrapper = operation_wrapper
13
+ @validate_success_only = validator_option.validate_success_only
14
+ @check_header = validator_option.check_header
15
+ end
16
+
17
+ def call(status, headers, response_data, strict)
18
+ return unless Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
19
+
20
+ operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :operation_wrapper, :check_header
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class Router
7
+ # @param [Committee::SchemaValidator::Option] validator_option
8
+ def initialize(schema, validator_option)
9
+ @schema = schema
10
+ @prefix_regexp = ::Committee::SchemaValidator.build_prefix_regexp(validator_option.prefix)
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def includes_request?(request)
15
+ return true unless @prefix_regexp
16
+
17
+ prefix_request?(request)
18
+ end
19
+
20
+ def build_schema_validator(request)
21
+ Committee::SchemaValidator::OpenAPI3.new(self, request, @validator_option)
22
+ end
23
+
24
+ def operation_object(request)
25
+ return nil unless includes_request?(request)
26
+
27
+ path = request.path
28
+ path = path.gsub(@prefix_regexp, '') if @prefix_regexp
29
+
30
+ request_method = request.request_method.downcase
31
+
32
+ @schema.operation_object(path, request_method)
33
+ end
34
+
35
+ private
36
+
37
+ def prefix_request?(request)
38
+ return false unless @prefix_regexp
39
+
40
+ request.path =~ @prefix_regexp
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ # @param [Committee::SchemaValidator::Option] validator_option
7
+ def initialize(router, request, validator_option)
8
+ @router = router
9
+ @request = request
10
+ @operation_object = router.operation_object(request)
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def request_validate(request)
15
+ return unless link_exist?
16
+
17
+ request_unpack(request)
18
+ request_schema_validation(request)
19
+
20
+ copy_coerced_data_to_params(request)
21
+ end
22
+
23
+ def response_validate(status, headers, response, test_method = false)
24
+ full_body = +""
25
+ response.each do |chunk|
26
+ full_body << chunk
27
+ end
28
+
29
+ parse_to_json = !validator_option.parse_response_by_content_type ||
30
+ headers.fetch('Content-Type', nil)&.start_with?('application/json')
31
+ data = if parse_to_json
32
+ full_body.empty? ? {} : JSON.parse(full_body)
33
+ else
34
+ full_body
35
+ end
36
+
37
+ # TODO: refactoring name
38
+ strict = test_method
39
+ Committee::SchemaValidator::OpenAPI3::ResponseValidator.
40
+ new(@operation_object, validator_option).
41
+ call(status, headers, data, strict)
42
+ end
43
+
44
+ def link_exist?
45
+ !@operation_object.nil?
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :validator_option
51
+
52
+ def coerce_path_params
53
+ return Committee::Utils.indifferent_hash unless validator_option.coerce_path_params
54
+ Committee::RequestUnpacker.indifferent_params(@operation_object.coerce_path_parameter(@validator_option))
55
+ end
56
+
57
+ def request_schema_validation(request)
58
+ return unless @operation_object
59
+
60
+ validator = Committee::SchemaValidator::OpenAPI3::RequestValidator.new(@operation_object, validator_option: validator_option)
61
+ validator.call(request, path_params(request), query_params(request), body_params(request), header(request))
62
+ end
63
+
64
+ def path_params(request)
65
+ request.env[validator_option.path_hash_key]
66
+ end
67
+
68
+ def query_params(request)
69
+ request.env[validator_option.query_hash_key]
70
+ end
71
+
72
+ def body_params(request)
73
+ request.env[validator_option.request_body_hash_key]
74
+ end
75
+
76
+ def header(request)
77
+ request.env[validator_option.headers_key]
78
+ end
79
+
80
+ def request_unpack(request)
81
+ unpacker = Committee::RequestUnpacker.new(
82
+ allow_form_params: validator_option.allow_form_params,
83
+ allow_get_body: validator_option.allow_get_body,
84
+ allow_query_params: validator_option.allow_query_params,
85
+ optimistic_json: validator_option.optimistic_json,
86
+ )
87
+
88
+ request.env[validator_option.headers_key] = unpacker.unpack_headers(request)
89
+
90
+ request_param, is_form_params = unpacker.unpack_request_params(request)
91
+ request.env[validator_option.request_body_hash_key] = request_param
92
+ request.env[validator_option.path_hash_key] = coerce_path_params
93
+
94
+ query_param = unpacker.unpack_query_params(request)
95
+ query_param.merge!(request_param) if request.get? && validator_option.allow_get_body
96
+ request.env[validator_option.query_hash_key] = query_param
97
+ end
98
+
99
+ def copy_coerced_data_to_params(request)
100
+ order = if validator_option.parameter_overwite_by_rails_rule
101
+ # (high priority) path_hash_key -> query_param -> request_body_hash
102
+ [validator_option.request_body_hash_key, validator_option.query_hash_key, validator_option.path_hash_key]
103
+ else
104
+ # (high priority) path_hash_key -> request_body_hash -> query_param
105
+ [validator_option.query_hash_key, validator_option.request_body_hash_key, validator_option.path_hash_key]
106
+ end
107
+
108
+ request.env[validator_option.params_key] = Committee::Utils.indifferent_hash
109
+ order.each do |key|
110
+ request.env[validator_option.params_key].merge!(Committee::Utils.deep_copy(request.env[key]))
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ require_relative "open_api_3/router"
118
+ require_relative "open_api_3/operation_wrapper"
119
+ require_relative "open_api_3/request_validator"
120
+ require_relative "open_api_3/response_validator"