committee_firetail 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 (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"