committee_firetail 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/bin/committee-stub +23 -0
  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 +36 -0
  22. data/lib/committee/middleware/base.rb +57 -0
  23. data/lib/committee/middleware/request_validation.rb +41 -0
  24. data/lib/committee/middleware/response_validation.rb +58 -0
  25. data/lib/committee/middleware/stub.rb +75 -0
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +91 -0
  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 +84 -0
  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 +26 -0
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +40 -0
  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 +130 -0
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +516 -0
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +189 -0
  64. data/test/middleware/stub_test.rb +145 -0
  65. data/test/request_unpacker_test.rb +200 -0
  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/schema_validator/hyper_schema/response_validator_test.rb +118 -0
  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 +363 -0
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +120 -0
  79. data/test/validation_error_test.rb +25 -0
  80. metadata +328 -0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ class RequestUnpacker
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
22
+
23
+ def initialize(options={})
24
+ @allow_form_params = options[:allow_form_params]
25
+ @allow_get_body = options[:allow_get_body]
26
+ @allow_query_params = options[:allow_query_params]
27
+ @optimistic_json = options[:optimistic_json]
28
+ end
29
+
30
+ # return params and is_form_params
31
+ def unpack_request_params(request)
32
+ # if Content-Type is empty or JSON, and there was a request body, try to
33
+ # interpret it as JSON
34
+ params = if !request.media_type || request.media_type =~ %r{application/(?:.*\+)?json}
35
+ parse_json(request)
36
+ elsif @optimistic_json
37
+ begin
38
+ parse_json(request)
39
+ rescue JSON::ParserError
40
+ nil
41
+ end
42
+ end
43
+
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)
47
+ # Actually, POST means anything in the request body, could be from
48
+ # PUT or PATCH too. Silly Rack.
49
+ return [request.POST, true] if request.POST
50
+ end
51
+
52
+ [{}, false]
53
+ end
54
+
55
+ def unpack_query_params(request)
56
+ @allow_query_params ? self.class.indifferent_params(request.GET) : {}
57
+ end
58
+
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
65
+ end
66
+
67
+ base['Content-Type'] = env['CONTENT_TYPE'] if env['CONTENT_TYPE']
68
+ base
69
+ end
70
+
71
+ private
72
+
73
+ def parse_json(request)
74
+ return nil if request.request_method == "GET" && !@allow_get_body
75
+
76
+ body = request.body.read
77
+ # if request body is empty, we just have empty params
78
+ return nil if body.length == 0
79
+
80
+ request.body.rewind
81
+ hash = JSON.parse(body)
82
+ # We want a hash specifically. '42', 42, and [42] will all be
83
+ # decoded properly, but we can't use them here.
84
+ if !hash.is_a?(Hash)
85
+ raise BadRequest,
86
+ "Invalid JSON input. Require object with parameters as keys."
87
+ end
88
+ self.class.indifferent_params(hash)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class ParameterCoercer
7
+ def initialize(params, schema, options = {})
8
+ @params = params
9
+ @schema = schema
10
+
11
+ @coerce_date_times = options.fetch(:coerce_date_times, false)
12
+ @coerce_recursive = options.fetch(:coerce_recursive, false)
13
+ end
14
+
15
+ def call!
16
+ coerce_object!(@params, @schema)
17
+ end
18
+
19
+ private
20
+
21
+ def coerce_object!(hash, schema)
22
+ return false unless schema.respond_to?(:properties)
23
+
24
+ is_coerced = false
25
+ schema.properties.each do |k, s|
26
+ original_val = hash[k]
27
+ unless original_val.nil?
28
+ new_value, is_changed = coerce_value!(original_val, s)
29
+ if is_changed
30
+ hash[k] = new_value
31
+ is_coerced = true
32
+ end
33
+ end
34
+ end
35
+
36
+ is_coerced
37
+ end
38
+
39
+ def coerce_value!(original_val, s)
40
+ s.type.each do |to_type|
41
+ if @coerce_date_times && to_type == "string" && s.format == "date-time"
42
+ coerced_val = parse_date_time(original_val)
43
+ return coerced_val, true if coerced_val
44
+ end
45
+
46
+ return original_val, true if @coerce_recursive && (to_type == "array") && coerce_array_data!(original_val, s)
47
+
48
+ return original_val, true if @coerce_recursive && (to_type == "object") && coerce_object!(original_val, s)
49
+ end
50
+ return nil, false
51
+ end
52
+
53
+ def coerce_array_data!(original_val, schema)
54
+ return false unless schema.respond_to?(:items)
55
+ return false unless original_val.is_a?(Array)
56
+
57
+ is_coerced = false
58
+ original_val.each_with_index do |d, index|
59
+ new_value, is_changed = coerce_value!(d, schema.items)
60
+ if is_changed
61
+ original_val[index] = new_value
62
+ is_coerced = true
63
+ end
64
+ end
65
+
66
+ is_coerced
67
+ end
68
+
69
+ def parse_date_time(original_val)
70
+ begin
71
+ DateTime.parse(original_val)
72
+ rescue ArgumentError => e
73
+ raise ::Committee::InvalidResponse unless e.message =~ /invalid date/
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class RequestValidator
7
+ def initialize(link, options = {})
8
+ @link = link
9
+ @check_content_type = options.fetch(:check_content_type, true)
10
+ @check_header = options.fetch(:check_header, true)
11
+ end
12
+
13
+ def call(request, params, headers)
14
+ check_content_type!(request, params) if @check_content_type
15
+ if @link.schema
16
+ valid, errors = @link.schema.validate(params)
17
+ if !valid
18
+ errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
19
+ raise InvalidRequest, "Invalid request.\n\n#{errors}"
20
+ end
21
+ end
22
+
23
+ if @check_header && @link.respond_to?(:header_schema) && @link.header_schema
24
+ valid, errors = @link.header_schema.validate(headers)
25
+ if !valid
26
+ errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
27
+ raise InvalidRequest, "Invalid request.\n\n#{errors}"
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def check_content_type!(request, data)
35
+ content_type = ::Committee::SchemaValidator.request_media_type(request)
36
+ if content_type && @link.enc_type && !empty_request?(request)
37
+ unless Rack::Mime.match?(content_type, @link.enc_type)
38
+ raise Committee::InvalidRequest,
39
+ %{"Content-Type" request header must be set to "#{@link.enc_type}".}
40
+ end
41
+ end
42
+ end
43
+
44
+ def empty_request?(request)
45
+ # small optimization: assume GET and DELETE don't have bodies
46
+ return true if request.get? || request.delete? || !request.body
47
+
48
+ data = request.body.read
49
+ request.body.rewind
50
+ data.empty?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class ResponseGenerator
7
+ def call(link)
8
+ schema = target_schema(link)
9
+ data = generate_properties(link, schema)
10
+
11
+ # List is a special case; wrap data in an array.
12
+ #
13
+ # This is poor form that's here so as not to introduce breaking behavior.
14
+ # The "instances" value of "rel" is a Heroku-ism and was originally
15
+ # introduced before we understood how to use "targetSchema". It's not
16
+ # meaningful with the context of the hyper-schema specification and
17
+ # should be eventually be removed.
18
+ if legacy_hyper_schema_rel?(link)
19
+ data = [data]
20
+ end
21
+
22
+ [data, schema]
23
+ end
24
+
25
+ private
26
+
27
+ # These are basic types that are part of the JSON schema for which we'll
28
+ # emit zero values when generating a response. For a schema that allows
29
+ # multiple of the types in the list, types are preferred in the order in
30
+ # which they're defined.
31
+ SCALAR_TYPES = {
32
+ "boolean" => false,
33
+ "integer" => 0,
34
+ "number" => 0.0,
35
+ "string" => "",
36
+
37
+ # Prefer null last.
38
+ "null" => nil,
39
+ }.freeze
40
+
41
+ def generate_properties(link, schema)
42
+ # special example attribute was included; use its value
43
+ if schema.data && !schema.data["example"].nil?
44
+ schema.data["example"]
45
+
46
+ elsif !schema.all_of.empty? || !schema.properties.empty?
47
+ data = {}
48
+ schema.all_of.each do |subschema|
49
+ data.merge!(generate_properties(link, subschema))
50
+ end
51
+ schema.properties.map do |key, value|
52
+ data[key] = generate_properties(link, value)
53
+ end
54
+ data
55
+
56
+ elsif schema.type.include?("array") && !schema.items.nil?
57
+ [generate_properties(link, schema.items)]
58
+
59
+ elsif schema.enum
60
+ schema.enum.first
61
+
62
+ elsif schema.type.any? { |t| SCALAR_TYPES.include?(t) }
63
+ SCALAR_TYPES.each do |k, v|
64
+ break(v) if schema.type.include?(k)
65
+ end
66
+
67
+ # Generate an empty array for arrays.
68
+ elsif schema.type == ["array"]
69
+ []
70
+
71
+ # Schema is an object with no properties: just generate an empty object.
72
+ elsif schema.type == ["object"]
73
+ {}
74
+
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.})
79
+ end
80
+ end
81
+
82
+ def legacy_hyper_schema_rel?(link)
83
+ link.is_a?(Committee::Drivers::HyperSchema::Link) &&
84
+ link.rel == "instances" &&
85
+ !link.target_schema
86
+ end
87
+
88
+ # Gets the target schema of a link. This is normally just the standard
89
+ # response schema, but we allow some legacy behavior for hyper-schema links
90
+ # tagged with rel=instances to instead use the schema of their parent
91
+ # resource.
92
+ def target_schema(link)
93
+ if link.target_schema
94
+ link.target_schema
95
+ elsif legacy_hyper_schema_rel?(link)
96
+ link.parent
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class ResponseValidator
7
+ attr_reader :validate_success_only
8
+
9
+ def initialize(link, options = {})
10
+ @link = link
11
+ @validate_success_only = options[:validate_success_only]
12
+
13
+ @validator = JsonSchema::Validator.new(target_schema(link))
14
+ end
15
+
16
+ def call(status, headers, data)
17
+ unless [204, 304].include?(status) # 204 No Content or 304 Not Modified
18
+ response = Rack::Response.new(data, status, headers)
19
+ check_content_type!(response)
20
+ end
21
+
22
+ # List is a special case; expect data in an array.
23
+ #
24
+ # This is poor form that's here so as not to introduce breaking behavior.
25
+ # The "instances" value of "rel" is a Heroku-ism and was originally
26
+ # introduced before we understood how to use "targetSchema". It's not
27
+ # meaningful with the context of the hyper-schema specification and
28
+ # should be eventually be removed.
29
+ if legacy_hyper_schema_rel?(@link)
30
+ if !data.is_a?(Array)
31
+ raise InvalidResponse, "List endpoints must return an array of objects."
32
+ end
33
+
34
+ # only consider the first object during the validation from here on
35
+ # (but only in cases where `targetSchema` is not set)
36
+ data = data[0]
37
+
38
+ # if the array was empty, allow it through
39
+ return if data == nil
40
+ end
41
+
42
+ if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
43
+ errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
44
+ raise InvalidResponse, "Invalid response.\n\n#{errors}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def response_media_type(response)
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
57
+ end
58
+
59
+
60
+ def check_content_type!(response)
61
+ if @link.media_type
62
+ unless Rack::Mime.match?(response_media_type(response), @link.media_type)
63
+ raise Committee::InvalidResponse,
64
+ %{"Content-Type" response header must be set to "#{@link.media_type}".}
65
+ end
66
+ end
67
+ end
68
+
69
+ def legacy_hyper_schema_rel?(link)
70
+ link.is_a?(Committee::Drivers::HyperSchema::Link) &&
71
+ link.rel == "instances" &&
72
+ !link.target_schema
73
+ end
74
+
75
+ # Gets the target schema of a link. This is normally just the standard
76
+ # response schema, but we allow some legacy behavior for hyper-schema links
77
+ # tagged with rel=instances to instead use the schema of their parent
78
+ # resource.
79
+ def target_schema(link)
80
+ if link.target_schema
81
+ link.target_schema
82
+ elsif legacy_hyper_schema_rel?(link)
83
+ link.parent
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -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"