committee 1.15.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +5 -5
  2. data/bin/committee-stub +11 -38
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +17 -0
  22. data/lib/committee/middleware/base.rb +46 -29
  23. data/lib/committee/middleware/request_validation.rb +31 -49
  24. data/lib/committee/middleware/response_validation.rb +48 -25
  25. data/lib/committee/middleware/stub.rb +62 -37
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +58 -50
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +68 -38
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +5 -2
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +31 -18
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +96 -7
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +423 -32
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +125 -23
  64. data/test/middleware/stub_test.rb +81 -20
  65. data/test/request_unpacker_test.rb +126 -52
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/{response_validator_test.rb → schema_validator/hyper_schema/response_validator_test.rb} +43 -6
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +334 -27
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +108 -1
  79. data/test/validation_error_test.rb +3 -1
  80. metadata +190 -27
  81. data/lib/committee/query_params_coercer.rb +0 -45
  82. data/lib/committee/request_validator.rb +0 -44
  83. data/lib/committee/response_generator.rb +0 -35
  84. data/lib/committee/response_validator.rb +0 -59
  85. data/lib/committee/router.rb +0 -62
  86. data/test/query_params_coercer_test.rb +0 -70
  87. data/test/request_validator_test.rb +0 -103
  88. data/test/response_generator_test.rb +0 -61
  89. data/test/router_test.rb +0 -38
@@ -1,45 +0,0 @@
1
- # Attempts to coerce params given in the query hash (which are all strings) into
2
- # the types specified by the schema.
3
- # Currently supported types: null, integer, number and boolean.
4
- # +call+ returns a hash of all params which could be coerced - coercion errors
5
- # are simply ignored and expected to be handled later by schema validation.
6
- module Committee
7
- class QueryParamsCoercer
8
- def initialize(query_hash, schema)
9
- @query_hash = query_hash
10
- @schema = schema
11
- end
12
-
13
- def call
14
- coerced = {}
15
- @schema.properties.each do |k, s|
16
- original_val = @query_hash[k]
17
- unless original_val.nil?
18
- s.type.each do |to_type|
19
- case to_type
20
- when "null"
21
- coerced[k] = nil if original_val.empty?
22
- when "integer"
23
- begin
24
- coerced[k] = Integer(original_val)
25
- rescue ArgumentError => e
26
- raise e unless e.message =~ /invalid value for Integer/
27
- end
28
- when "number"
29
- begin
30
- coerced[k] = Float(original_val)
31
- rescue ArgumentError => e
32
- raise e unless e.message =~ /invalid value for Float/
33
- end
34
- when "boolean"
35
- coerced[k] = true if original_val == "true"
36
- coerced[k] = false if original_val == "false"
37
- end
38
- break if coerced.key?(k)
39
- end
40
- end
41
- end
42
- coerced
43
- end
44
- end
45
- end
@@ -1,44 +0,0 @@
1
- module Committee
2
- class RequestValidator
3
- def initialize(link, options = {})
4
- @link = link
5
- @check_content_type = options.fetch(:check_content_type, true)
6
- end
7
-
8
- def call(request, data)
9
- check_content_type!(request, data) if @check_content_type
10
- if @link.schema
11
- valid, errors = @link.schema.validate(data)
12
- if !valid
13
- errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
14
- raise InvalidRequest, "Invalid request.\n\n#{errors}"
15
- end
16
- end
17
- end
18
-
19
- private
20
-
21
- def request_media_type(request)
22
- request.content_type.to_s.split(";").first.to_s
23
- end
24
-
25
- def check_content_type!(request, data)
26
- content_type = request_media_type(request)
27
- if content_type && !empty_request?(request)
28
- unless Rack::Mime.match?(content_type, @link.enc_type)
29
- raise Committee::InvalidRequest,
30
- %{"Content-Type" request header must be set to "#{@link.enc_type}".}
31
- end
32
- end
33
- end
34
-
35
- def empty_request?(request)
36
- # small optimization: assume GET and DELETE don't have bodies
37
- return true if request.get? || request.delete? || !request.body
38
-
39
- data = request.body.read
40
- request.body.rewind
41
- data.empty?
42
- end
43
- end
44
- end
@@ -1,35 +0,0 @@
1
- module Committee
2
- class ResponseGenerator
3
- def call(link)
4
- data = generate_properties(link.target_schema || link.parent)
5
-
6
- # list is a special case; wrap data in an array
7
- data = [data] if link.rel == "instances"
8
-
9
- data
10
- end
11
-
12
- private
13
-
14
- def generate_properties(schema)
15
- # special example attribute was included; use its value
16
- if !schema.data["example"].nil?
17
- schema.data["example"]
18
- # null is allowed; use that
19
- elsif schema.type.include?("null")
20
- nil
21
- elsif schema.type.include?("array") && !schema.items.nil?
22
- [generate_properties(schema.items)]
23
- elsif !schema.properties.empty?
24
- data = {}
25
- schema.properties.map do |key, value|
26
- data[key] = generate_properties(value)
27
- end
28
- data
29
- else
30
- raise(%{At "#{schema.pointer}": no "example" attribute and "null" } +
31
- %{is not allowed; don't know how to generate property.})
32
- end
33
- end
34
- end
35
- end
@@ -1,59 +0,0 @@
1
- module Committee
2
- class ResponseValidator
3
- attr_reader :validate_errors
4
-
5
- def initialize(link, options = {})
6
- @link = link
7
- @validate_errors = options[:validate_errors]
8
-
9
- # we should eventually move off of validating against parent schema too
10
- # ... this is a Herokuism and not in the specification
11
- schema = link.target_schema || link.parent
12
- @validator = JsonSchema::Validator.new(schema)
13
- end
14
-
15
- def self.validate?(status, options = {})
16
- validate_errors = options[:validate_errors]
17
-
18
- status != 204 and validate_errors || (200...300).include?(status)
19
- end
20
-
21
- def call(status, headers, data)
22
- unless status == 204 # 204 No Content
23
- response = Rack::Response.new(data, status, headers)
24
- check_content_type!(response)
25
- end
26
-
27
- if @link.rel == "instances" && !@link.target_schema
28
- if !data.is_a?(Array)
29
- raise InvalidResponse, "List endpoints must return an array of objects."
30
- end
31
-
32
- # only consider the first object during the validation from here on
33
- # (but only in cases where `targetSchema` is not set)
34
- data = data[0]
35
-
36
- # if the array was empty, allow it through
37
- return if data == nil
38
- end
39
-
40
- if self.class.validate?(status, validate_errors: validate_errors) && !@validator.validate(data)
41
- errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
42
- raise InvalidResponse, "Invalid response.\n\n#{errors}"
43
- end
44
- end
45
-
46
- private
47
-
48
- def response_media_type(response)
49
- response.content_type.to_s.split(";").first.to_s
50
- end
51
-
52
- def check_content_type!(response)
53
- unless Rack::Mime.match?(response_media_type(response), @link.media_type)
54
- raise Committee::InvalidResponse,
55
- %{"Content-Type" response header must be set to "#{@link.media_type}".}
56
- end
57
- end
58
- end
59
- end
@@ -1,62 +0,0 @@
1
- module Committee
2
- class Router
3
- def initialize(schema, options = {})
4
- @routes = build_routes(schema)
5
- @prefix = options[:prefix]
6
- @prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
7
- end
8
-
9
- def includes?(path)
10
- !@prefix || path =~ @prefix_regexp
11
- end
12
-
13
- def includes_request?(request)
14
- includes?(request.path)
15
- end
16
-
17
- def find_link(method, path)
18
- path = path.gsub(@prefix_regexp, "") if @prefix
19
- if method_routes = @routes[method]
20
- method_routes.each do |pattern, link|
21
- if path =~ pattern
22
- return link
23
- end
24
- end
25
- end
26
- nil
27
- end
28
-
29
- def find_request_link(request)
30
- find_link(request.request_method, request.path_info)
31
- end
32
-
33
- private
34
-
35
- def build_routes(schema)
36
- routes = {}
37
-
38
- schema.links.each do |link|
39
- method, href = parse_link(link)
40
- next unless method
41
- routes[method] ||= []
42
- routes[method] << [%r{^#{href}$}, link]
43
- end
44
-
45
- # recursively iterate through all `properties` subschemas to build a
46
- # complete routing table
47
- schema.properties.each do |_, subschema|
48
- routes.merge!(build_routes(subschema)) { |_, r1, r2| r1 + r2 }
49
- end
50
-
51
- routes
52
- end
53
-
54
- def parse_link(link)
55
- return nil, nil if !link.method || !link.href
56
- method = link.method.to_s.upcase
57
- # /apps/{id} --> /apps/([^/]+)
58
- href = link.href.gsub(/\{(.*?)\}/, "[^/]+")
59
- [method, href]
60
- end
61
- end
62
- end
@@ -1,70 +0,0 @@
1
- require_relative "test_helper"
2
-
3
- describe Committee::QueryParamsCoercer do
4
- before do
5
- @schema =
6
- JsonSchema.parse!(JSON.parse(File.read("./test/data/schema.json")))
7
- @schema.expand_references!
8
- # GET /search/apps
9
- @link = @link = @schema.properties["app"].links[5]
10
- end
11
-
12
- it "doesn't coerce params not in the schema" do
13
- data = {"owner" => "admin"}
14
- assert_equal({}, call(data))
15
- end
16
-
17
- it "skips values for string param" do
18
- data = {"name" => "foo"}
19
- assert_equal({}, call(data))
20
- end
21
-
22
- it "coerces valid values for boolean param" do
23
- data = {"deleted" => "true"}
24
- assert_equal({"deleted" => true}, call(data))
25
- data = {"deleted" => "false"}
26
- assert_equal({"deleted" => false}, call(data))
27
- end
28
-
29
- it "skips invalid values for boolean param" do
30
- data = {"deleted" => "foo"}
31
- assert_equal({}, call(data))
32
- end
33
-
34
- it "coerces valid values for integer param" do
35
- data = {"per_page" => "3"}
36
- assert_equal({"per_page" => 3}, call(data))
37
- end
38
-
39
- it "skips invalid values for integer param" do
40
- data = {"per_page" => "3.5"}
41
- assert_equal({}, call(data))
42
- data = {"per_page" => "false"}
43
- assert_equal({}, call(data))
44
- data = {"per_page" => ""}
45
- assert_equal({}, call(data))
46
- end
47
-
48
- it "coerces valid values for number param" do
49
- data = {"threshold" => "3"}
50
- assert_equal({"threshold" => 3.0}, call(data))
51
- data = {"threshold" => "3.5"}
52
- assert_equal({"threshold" => 3.5}, call(data))
53
- end
54
-
55
- it "skips invalid values for number param" do
56
- data = {"threshold" => "false"}
57
- assert_equal({}, call(data))
58
- end
59
-
60
- it "coerces valid values for null param" do
61
- data = {"threshold" => ""}
62
- assert_equal({"threshold" => nil}, call(data))
63
- end
64
-
65
- private
66
-
67
- def call(data)
68
- Committee::QueryParamsCoercer.new(data, @link.schema).call
69
- end
70
- end
@@ -1,103 +0,0 @@
1
- require_relative "test_helper"
2
-
3
- require "stringio"
4
-
5
- describe Committee::RequestValidator do
6
- before do
7
- @schema =
8
- JsonSchema.parse!(JSON.parse(File.read("./test/data/schema.json")))
9
- @schema.expand_references!
10
- # POST /apps/:id
11
- @link = @link = @schema.properties["app"].links[0]
12
- @request = Rack::Request.new({
13
- "CONTENT_TYPE" => "application/json",
14
- "rack.input" => StringIO.new("{}"),
15
- "REQUEST_METHOD" => "POST"
16
- })
17
- end
18
-
19
- it "passes through a valid request with Content-Type options" do
20
- @headers = { "Content-Type" => "application/json; charset=utf-8" }
21
- call({})
22
- end
23
-
24
- it "passes through a valid request" do
25
- data = {
26
- "name" => "heroku-api",
27
- }
28
- call(data)
29
- end
30
-
31
- it "passes through a valid request with Content-Type options" do
32
- @request =
33
- Rack::Request.new({
34
- "CONTENT_TYPE" => "application/json; charset=utf-8",
35
- "rack.input" => StringIO.new("{}"),
36
- })
37
- data = {
38
- "name" => "heroku-api",
39
- }
40
- call(data)
41
- end
42
-
43
- it "detects an invalid request Content-Type" do
44
- e = assert_raises(Committee::InvalidRequest) {
45
- @request =
46
- Rack::Request.new({
47
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
48
- "rack.input" => StringIO.new("{}"),
49
- })
50
- call({})
51
- }
52
- message =
53
- %{"Content-Type" request header must be set to "application/json".}
54
- assert_equal message, e.message
55
- end
56
-
57
- it "allows skipping content_type check" do
58
- @request =
59
- Rack::Request.new({
60
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
61
- "rack.input" => StringIO.new("{}"),
62
- })
63
- call({}, check_content_type: false)
64
- end
65
-
66
- it "detects an missing parameter in GET requests" do
67
- # GET /apps/search?query=...
68
- @link = @link = @schema.properties["app"].links[5]
69
- @request = Rack::Request.new({})
70
- e = assert_raises(Committee::InvalidRequest) do
71
- call({})
72
- end
73
- message =
74
- %{Invalid request.\n\n#: failed schema #/definitions/app/links/5/schema: "query" wasn't supplied.}
75
- assert_equal message, e.message
76
- end
77
-
78
- it "allows an invalid Content-Type with an empty body" do
79
- @request =
80
- Rack::Request.new({
81
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
82
- "rack.input" => StringIO.new(""),
83
- })
84
- call({})
85
- end
86
-
87
- it "detects a parameter of the wrong pattern" do
88
- data = {
89
- "name" => "%@!"
90
- }
91
- e = assert_raises(Committee::InvalidRequest) do
92
- call(data)
93
- end
94
- message = %{Invalid request.\n\n#/name: failed schema #/definitions/app/links/0/schema/properties/name: %@! does not match /^[a-z][a-z0-9-]{3,30}$/.}
95
- assert_equal message, e.message
96
- end
97
-
98
- private
99
-
100
- def call(data, options={})
101
- Committee::RequestValidator.new(@link, options).call(@request, data)
102
- end
103
- end
@@ -1,61 +0,0 @@
1
- require_relative "test_helper"
2
-
3
- describe Committee::ResponseGenerator do
4
- before do
5
- @schema =
6
- JsonSchema.parse!(JSON.parse(File.read("./test/data/schema.json")))
7
- @schema.expand_references!
8
- # GET /apps/:id
9
- @get_link = @link = @schema.properties["app"].links[2]
10
- # GET /apps
11
- @list_link = @schema.properties["app"].links[3]
12
- end
13
-
14
- it "generates string properties" do
15
- data = call
16
- assert data["name"].is_a?(String)
17
- end
18
-
19
- it "generates non-string properties" do
20
- data = call
21
- assert_includes [FalseClass, TrueClass], data["maintenance"].class
22
- end
23
-
24
- it "wraps list data in an array" do
25
- @link = @list_link
26
- data = call
27
- assert data.is_a?(Array)
28
- end
29
-
30
- it "wraps list data in an array" do
31
- @link = @list_link
32
-
33
- @link.rel = nil
34
-
35
- data = call
36
- assert data.is_a?(Array)
37
- end
38
-
39
- it "errors with no known route for generation" do
40
- @link = @get_link
41
-
42
- # tweak the schema so that it can't be generated
43
- property = @schema.properties["app"].properties["maintenance"]
44
- property.data["example"] = nil
45
- property.type = []
46
-
47
- e = assert_raises(RuntimeError) do
48
- call
49
- end
50
-
51
- expected = <<-eos.gsub(/\n +/, "").strip
52
- At "#/definitions/app/properties/maintenance": no "example" attribute and
53
- "null" is not allowed; don't know how to generate property.
54
- eos
55
- assert_equal expected, e.message
56
- end
57
-
58
- def call
59
- Committee::ResponseGenerator.new.call(@link)
60
- end
61
- end
data/test/router_test.rb DELETED
@@ -1,38 +0,0 @@
1
- require_relative "test_helper"
2
-
3
- describe Committee::Router do
4
- it "builds routes without parameters" do
5
- refute_nil router.find_link("GET", "/apps")
6
- end
7
-
8
- it "builds routes with parameters" do
9
- refute_nil router.find_link("GET", "/apps/123")
10
- end
11
-
12
- it "doesn't match anything on a /" do
13
- assert_nil router.find_link("GET", "/")
14
- end
15
-
16
- it "takes a prefix" do
17
- # this is a sociopathic example
18
- assert router(prefix: "/kpi").find_link("GET", "/kpi/apps/123")
19
- end
20
-
21
- it "includes all paths without a prefix" do
22
- assert router.includes?("/")
23
- assert router.includes?("/apps")
24
- end
25
-
26
- it "only includes the prefix path with a prefix" do
27
- refute router(prefix: "/kpi").includes?("/")
28
- assert router(prefix: "/kpi").includes?("/kpi")
29
- assert router(prefix: "/kpi").includes?("/kpi/apps")
30
- end
31
-
32
- def router(options = {})
33
- data = JSON.parse(File.read("./test/data/schema.json"))
34
- schema = JsonSchema.parse!(data)
35
- schema.expand_references!
36
- Committee::Router.new(schema, options)
37
- end
38
- end