committee 3.1.0 → 3.1.1

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