committee 3.1.0 → 3.1.1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/bin/committee-stub +1 -0
  3. data/lib/committee.rb +12 -34
  4. data/lib/committee/bin/committee_stub.rb +6 -4
  5. data/lib/committee/drivers.rb +15 -67
  6. data/lib/committee/drivers/driver.rb +47 -0
  7. data/lib/committee/drivers/hyper_schema.rb +8 -171
  8. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  9. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  10. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  11. data/lib/committee/drivers/open_api_2.rb +9 -416
  12. data/lib/committee/drivers/open_api_2/driver.rb +253 -0
  13. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  14. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  15. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  16. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  17. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  18. data/lib/committee/drivers/open_api_3.rb +7 -75
  19. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  20. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  21. data/lib/committee/drivers/schema.rb +23 -0
  22. data/lib/committee/errors.rb +2 -0
  23. data/lib/committee/middleware.rb +11 -0
  24. data/lib/committee/middleware/base.rb +38 -34
  25. data/lib/committee/middleware/request_validation.rb +51 -30
  26. data/lib/committee/middleware/response_validation.rb +49 -26
  27. data/lib/committee/middleware/stub.rb +55 -51
  28. data/lib/committee/request_unpacker.rb +3 -1
  29. data/lib/committee/schema_validator.rb +23 -0
  30. data/lib/committee/schema_validator/hyper_schema.rb +85 -74
  31. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +60 -54
  32. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +43 -37
  33. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +86 -80
  34. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +65 -59
  35. data/lib/committee/schema_validator/hyper_schema/router.rb +35 -29
  36. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +87 -81
  37. data/lib/committee/schema_validator/open_api_3.rb +71 -61
  38. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +121 -115
  39. data/lib/committee/schema_validator/open_api_3/request_validator.rb +24 -18
  40. data/lib/committee/schema_validator/open_api_3/response_validator.rb +22 -16
  41. data/lib/committee/schema_validator/open_api_3/router.rb +30 -24
  42. data/lib/committee/schema_validator/option.rb +42 -38
  43. data/lib/committee/test/methods.rb +55 -51
  44. data/lib/committee/validation_error.rb +2 -0
  45. data/test/bin/committee_stub_test.rb +3 -1
  46. data/test/bin_test.rb +3 -1
  47. data/test/committee_test.rb +3 -1
  48. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  49. data/test/drivers/{hyper_schema_test.rb → hyper_schema/link_test.rb} +2 -45
  50. data/test/drivers/open_api_2/driver_test.rb +156 -0
  51. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  52. data/test/drivers/open_api_2/link_test.rb +52 -0
  53. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  54. data/test/drivers/{open_api_3_test.rb → open_api_3/driver_test.rb} +5 -3
  55. data/test/drivers_test.rb +12 -10
  56. data/test/middleware/base_test.rb +3 -1
  57. data/test/middleware/request_validation_open_api_3_test.rb +4 -2
  58. data/test/middleware/request_validation_test.rb +46 -5
  59. data/test/middleware/response_validation_open_api_3_test.rb +3 -1
  60. data/test/middleware/response_validation_test.rb +39 -4
  61. data/test/middleware/stub_test.rb +3 -1
  62. data/test/request_unpacker_test.rb +2 -2
  63. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +2 -2
  64. data/test/schema_validator/hyper_schema/request_validator_test.rb +3 -1
  65. data/test/schema_validator/hyper_schema/response_generator_test.rb +3 -1
  66. data/test/schema_validator/hyper_schema/response_validator_test.rb +3 -1
  67. data/test/schema_validator/hyper_schema/router_test.rb +5 -3
  68. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +3 -1
  69. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +3 -1
  70. data/test/schema_validator/open_api_3/request_validator_test.rb +11 -1
  71. data/test/schema_validator/open_api_3/response_validator_test.rb +3 -1
  72. data/test/test/methods_new_version_test.rb +3 -1
  73. data/test/test/methods_test.rb +4 -2
  74. data/test/test_helper.rb +16 -16
  75. data/test/validation_error_test.rb +3 -1
  76. metadata +52 -6
  77. data/lib/committee/schema_validator/schema_validator.rb +0 -15
  78. data/test/drivers/open_api_2_test.rb +0 -416
@@ -1,49 +1,55 @@
1
- module Committee
2
- class SchemaValidator::HyperSchema::RequestValidator
3
- def initialize(link, options = {})
4
- @link = link
5
- @check_content_type = options.fetch(:check_content_type, true)
6
- @check_header = options.fetch(:check_header, true)
7
- end
1
+ # frozen_string_literal: true
8
2
 
9
- def call(request, params, headers)
10
- check_content_type!(request, params) if @check_content_type
11
- if @link.schema
12
- valid, errors = @link.schema.validate(params)
13
- if !valid
14
- errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
15
- raise InvalidRequest, "Invalid request.\n\n#{errors}"
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)
16
11
  end
17
- end
18
12
 
19
- if @check_header && @link.respond_to?(:header_schema) && @link.header_schema
20
- valid, errors = @link.header_schema.validate(headers)
21
- if !valid
22
- errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
23
- raise InvalidRequest, "Invalid request.\n\n#{errors}"
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
24
30
  end
25
- end
26
- end
27
31
 
28
- private
32
+ private
29
33
 
30
- def check_content_type!(request, data)
31
- content_type = ::Committee::SchemaValidator.request_media_type(request)
32
- if content_type && @link.enc_type && !empty_request?(request)
33
- unless Rack::Mime.match?(content_type, @link.enc_type)
34
- raise Committee::InvalidRequest,
35
- %{"Content-Type" request header must be set to "#{@link.enc_type}".}
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
36
42
  end
37
- end
38
- end
39
43
 
40
- def empty_request?(request)
41
- # small optimization: assume GET and DELETE don't have bodies
42
- return true if request.get? || request.delete? || !request.body
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
43
47
 
44
- data = request.body.read
45
- request.body.rewind
46
- data.empty?
48
+ data = request.body.read
49
+ request.body.rewind
50
+ data.empty?
51
+ end
52
+ end
47
53
  end
48
54
  end
49
55
  end
@@ -1,95 +1,101 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Committee
2
- class SchemaValidator::HyperSchema::ResponseGenerator
3
- def call(link)
4
- schema = target_schema(link)
5
- data = generate_properties(link, schema)
6
-
7
- # List is a special case; wrap data in an array.
8
- #
9
- # This is poor form that's here so as not to introduce breaking behavior.
10
- # The "instances" value of "rel" is a Heroku-ism and was originally
11
- # introduced before we understood how to use "targetSchema". It's not
12
- # meaningful with the context of the hyper-schema specification and
13
- # should be eventually be removed.
14
- if legacy_hyper_schema_rel?(link)
15
- data = [data]
16
- end
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class ResponseGenerator
7
+ def call(link)
8
+ schema = target_schema(link)
9
+ data = generate_properties(link, schema)
17
10
 
18
- [data, schema]
19
- end
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
20
21
 
21
- private
22
-
23
- # These are basic types that are part of the JSON schema for which we'll
24
- # emit zero values when generating a response. For a schema that allows
25
- # multiple of the types in the list, types are preferred in the order in
26
- # which they're defined.
27
- SCALAR_TYPES = {
28
- "boolean" => false,
29
- "integer" => 0,
30
- "number" => 0.0,
31
- "string" => "",
32
-
33
- # Prefer null last.
34
- "null" => nil,
35
- }.freeze
36
-
37
- def generate_properties(link, schema)
38
- # special example attribute was included; use its value
39
- if schema.data && !schema.data["example"].nil?
40
- schema.data["example"]
41
-
42
- elsif !schema.all_of.empty? || !schema.properties.empty?
43
- data = {}
44
- schema.all_of.each do |subschema|
45
- data.merge!(generate_properties(link, subschema))
46
- end
47
- schema.properties.map do |key, value|
48
- data[key] = generate_properties(link, value)
22
+ [data, schema]
49
23
  end
50
- data
51
24
 
52
- elsif schema.type.include?("array") && !schema.items.nil?
53
- [generate_properties(link, schema.items)]
25
+ private
54
26
 
55
- elsif schema.enum
56
- schema.enum.first
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" => "",
57
36
 
58
- elsif schema.type.any? { |t| SCALAR_TYPES.include?(t) }
59
- SCALAR_TYPES.each do |k, v|
60
- break(v) if schema.type.include?(k)
61
- end
37
+ # Prefer null last.
38
+ "null" => nil,
39
+ }.freeze
62
40
 
63
- # Generate an empty array for arrays.
64
- elsif schema.type == ["array"]
65
- []
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"]
66
45
 
67
- # Schema is an object with no properties: just generate an empty object.
68
- elsif schema.type == ["object"]
69
- {}
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
70
55
 
71
- else
72
- raise(%{At "#{link.method} #{link.href}" "#{schema.pointer}": no } +
73
- %{"example" attribute and "null" } +
74
- %{is not allowed; don't know how to generate property.})
75
- end
76
- end
56
+ elsif schema.type.include?("array") && !schema.items.nil?
57
+ [generate_properties(link, schema.items)]
77
58
 
78
- def legacy_hyper_schema_rel?(link)
79
- link.is_a?(Committee::Drivers::HyperSchema::Link) &&
80
- link.rel == "instances" &&
81
- !link.target_schema
82
- end
59
+ elsif schema.enum
60
+ schema.enum.first
83
61
 
84
- # Gets the target schema of a link. This is normally just the standard
85
- # response schema, but we allow some legacy behavior for hyper-schema links
86
- # tagged with rel=instances to instead use the schema of their parent
87
- # resource.
88
- def target_schema(link)
89
- if link.target_schema
90
- link.target_schema
91
- elsif legacy_hyper_schema_rel?(link)
92
- link.parent
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
93
99
  end
94
100
  end
95
101
  end
@@ -1,76 +1,82 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Committee
2
- class SchemaValidator::HyperSchema::ResponseValidator
3
- attr_reader :validate_success_only
4
+ module SchemaValidator
5
+ class HyperSchema
6
+ class ResponseValidator
7
+ attr_reader :validate_success_only
4
8
 
5
- def initialize(link, options = {})
6
- @link = link
7
- @validate_success_only = options[:validate_success_only]
9
+ def initialize(link, options = {})
10
+ @link = link
11
+ @validate_success_only = options[:validate_success_only]
8
12
 
9
- @validator = JsonSchema::Validator.new(target_schema(link))
10
- end
13
+ @validator = JsonSchema::Validator.new(target_schema(link))
14
+ end
11
15
 
12
- def call(status, headers, data)
13
- unless status == 204 # 204 No Content
14
- response = Rack::Response.new(data, status, headers)
15
- check_content_type!(response)
16
- end
16
+ def call(status, headers, data)
17
+ unless status == 204 # 204 No Content
18
+ response = Rack::Response.new(data, status, headers)
19
+ check_content_type!(response)
20
+ end
17
21
 
18
- # List is a special case; expect data in an array.
19
- #
20
- # This is poor form that's here so as not to introduce breaking behavior.
21
- # The "instances" value of "rel" is a Heroku-ism and was originally
22
- # introduced before we understood how to use "targetSchema". It's not
23
- # meaningful with the context of the hyper-schema specification and
24
- # should be eventually be removed.
25
- if legacy_hyper_schema_rel?(@link)
26
- if !data.is_a?(Array)
27
- raise InvalidResponse, "List endpoints must return an array of objects."
28
- end
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
29
33
 
30
- # only consider the first object during the validation from here on
31
- # (but only in cases where `targetSchema` is not set)
32
- data = data[0]
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]
33
37
 
34
- # if the array was empty, allow it through
35
- return if data == nil
36
- end
38
+ # if the array was empty, allow it through
39
+ return if data == nil
40
+ end
37
41
 
38
- if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
39
- errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
40
- raise InvalidResponse, "Invalid response.\n\n#{errors}"
41
- end
42
- end
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
43
47
 
44
- private
48
+ private
45
49
 
46
- def response_media_type(response)
47
- response.content_type.to_s.split(";").first.to_s
48
- end
50
+ def response_media_type(response)
51
+ response.content_type.to_s.split(";").first.to_s
52
+ end
49
53
 
50
- def check_content_type!(response)
51
- if @link.media_type
52
- unless Rack::Mime.match?(response_media_type(response), @link.media_type)
53
- raise Committee::InvalidResponse,
54
- %{"Content-Type" response header must be set to "#{@link.media_type}".}
54
+ def check_content_type!(response)
55
+ if @link.media_type
56
+ unless Rack::Mime.match?(response_media_type(response), @link.media_type)
57
+ raise Committee::InvalidResponse,
58
+ %{"Content-Type" response header must be set to "#{@link.media_type}".}
59
+ end
60
+ end
55
61
  end
56
- end
57
- end
58
62
 
59
- def legacy_hyper_schema_rel?(link)
60
- link.is_a?(Committee::Drivers::HyperSchema::Link) &&
61
- link.rel == "instances" &&
62
- !link.target_schema
63
- end
63
+ def legacy_hyper_schema_rel?(link)
64
+ link.is_a?(Committee::Drivers::HyperSchema::Link) &&
65
+ link.rel == "instances" &&
66
+ !link.target_schema
67
+ end
64
68
 
65
- # Gets the target schema of a link. This is normally just the standard
66
- # response schema, but we allow some legacy behavior for hyper-schema links
67
- # tagged with rel=instances to instead use the schema of their parent
68
- # resource.
69
- def target_schema(link)
70
- if link.target_schema
71
- link.target_schema
72
- elsif legacy_hyper_schema_rel?(link)
73
- link.parent
69
+ # Gets the target schema of a link. This is normally just the standard
70
+ # response schema, but we allow some legacy behavior for hyper-schema links
71
+ # tagged with rel=instances to instead use the schema of their parent
72
+ # resource.
73
+ def target_schema(link)
74
+ if link.target_schema
75
+ link.target_schema
76
+ elsif legacy_hyper_schema_rel?(link)
77
+ link.parent
78
+ end
79
+ end
74
80
  end
75
81
  end
76
82
  end
@@ -1,40 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Committee
2
- class SchemaValidator::HyperSchema::Router
3
- def initialize(schema, validator_option)
4
- @prefix = validator_option.prefix
5
- @prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
6
- @schema = schema
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
7
11
 
8
- @validator_option = validator_option
9
- end
12
+ @validator_option = validator_option
13
+ end
10
14
 
11
- def includes?(path)
12
- !@prefix || path =~ @prefix_regexp
13
- end
15
+ def includes?(path)
16
+ !@prefix || path =~ @prefix_regexp
17
+ end
14
18
 
15
- def includes_request?(request)
16
- includes?(request.path)
17
- end
19
+ def includes_request?(request)
20
+ includes?(request.path)
21
+ end
18
22
 
19
- def find_link(method, path)
20
- path = path.gsub(@prefix_regexp, "") if @prefix
21
- link_with_matches = (@schema.routes[method] || []).map do |pattern, link|
22
- if matches = pattern.match(path)
23
- # prefer path which has fewer matches (eg. `/pets/dog` than `/pets/{uuid}` for path `/pets/dog` )
24
- [matches.captures.size, link, Hash[matches.names.zip(matches.captures)]]
25
- else
26
- nil
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)
27
34
  end
28
- end.compact.sort_by(&:first).first
29
- link_with_matches.nil? ? nil : link_with_matches.slice(1, 2)
30
- end
31
35
 
32
- def find_request_link(request)
33
- find_link(request.request_method, request.path_info)
34
- end
36
+ def find_request_link(request)
37
+ find_link(request.request_method, request.path_info)
38
+ end
35
39
 
36
- def build_schema_validator(request)
37
- Committee::SchemaValidator::HyperSchema.new(self, request, @validator_option)
40
+ def build_schema_validator(request)
41
+ Committee::SchemaValidator::HyperSchema.new(self, request, @validator_option)
42
+ end
43
+ end
38
44
  end
39
45
  end
40
46
  end