committee 1.15.0 → 2.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/committee-stub +10 -36
- data/lib/committee/bin/committee_stub.rb +61 -0
- data/lib/committee/drivers/hyper_schema.rb +151 -0
- data/lib/committee/drivers/open_api_2.rb +297 -0
- data/lib/committee/drivers.rb +57 -0
- data/lib/committee/middleware/base.rb +52 -13
- data/lib/committee/middleware/request_validation.rb +33 -10
- data/lib/committee/middleware/response_validation.rb +2 -1
- data/lib/committee/middleware/stub.rb +24 -8
- data/lib/committee/request_validator.rb +1 -1
- data/lib/committee/response_generator.rb +58 -13
- data/lib/committee/response_validator.rb +32 -8
- data/lib/committee/router.rb +5 -33
- data/lib/committee/{query_params_coercer.rb → string_params_coercer.rb} +11 -6
- data/lib/committee/test/methods.rb +49 -12
- data/lib/committee.rb +15 -1
- data/test/bin/committee_stub_test.rb +45 -0
- data/test/bin_test.rb +20 -0
- data/test/committee_test.rb +49 -0
- data/test/drivers/hyper_schema_test.rb +95 -0
- data/test/drivers/open_api_2_test.rb +255 -0
- data/test/drivers_test.rb +60 -0
- data/test/middleware/base_test.rb +49 -5
- data/test/middleware/request_validation_test.rb +39 -25
- data/test/middleware/response_validation_test.rb +32 -20
- data/test/middleware/stub_test.rb +50 -19
- data/test/request_unpacker_test.rb +10 -0
- data/test/request_validator_test.rb +4 -3
- data/test/response_generator_test.rb +50 -6
- data/test/response_validator_test.rb +29 -4
- data/test/router_test.rb +40 -13
- data/test/{query_params_coercer_test.rb → string_params_coercer_test.rb} +3 -4
- data/test/test/methods_test.rb +44 -5
- data/test/test_helper.rb +59 -1
- metadata +62 -10
@@ -6,19 +6,12 @@ module Committee::Middleware
|
|
6
6
|
@error_class = options.fetch(:error_class, Committee::ValidationError)
|
7
7
|
@params_key = options[:params_key] || "committee.params"
|
8
8
|
@raise = options[:raise]
|
9
|
+
@schema = get_schema(options[:schema] ||
|
10
|
+
raise(ArgumentError, "Committee: need option `schema`"))
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
schema = JSON.parse(schema)
|
14
|
-
end
|
15
|
-
if schema.is_a?(Hash)
|
16
|
-
schema = JsonSchema.parse!(schema)
|
17
|
-
schema.expand_references!
|
18
|
-
end
|
19
|
-
@schema = schema
|
20
|
-
|
21
|
-
@router = Committee::Router.new(@schema, options)
|
12
|
+
@router = Committee::Router.new(@schema,
|
13
|
+
prefix: options[:prefix]
|
14
|
+
)
|
22
15
|
end
|
23
16
|
|
24
17
|
def call(env)
|
@@ -33,8 +26,54 @@ module Committee::Middleware
|
|
33
26
|
|
34
27
|
private
|
35
28
|
|
29
|
+
# For modern use of the library a schema should be an instance of
|
30
|
+
# Committee::Drivers::Schema so that we know that all the computationally
|
31
|
+
# difficult parsing is already done by the time we try to handle any
|
32
|
+
# request.
|
33
|
+
#
|
34
|
+
# However, for reasons of backwards compatibility we also allow schema
|
35
|
+
# input to be a string, a data hash, or a JsonSchema::Schema. In the former
|
36
|
+
# two cases we just parse as if we were sent hyper-schema. In the latter,
|
37
|
+
# we have the hyper-schema driver wrap it in a new Committee object.
|
38
|
+
def get_schema(schema)
|
39
|
+
# These are in a separately conditional ladder so that we only show the
|
40
|
+
# user one warning.
|
41
|
+
if schema.is_a?(String)
|
42
|
+
warn_string_deprecated
|
43
|
+
elsif schema.is_a?(Hash)
|
44
|
+
warn_hash_deprecated
|
45
|
+
end
|
46
|
+
|
47
|
+
if schema.is_a?(String)
|
48
|
+
schema = JSON.parse(schema)
|
49
|
+
end
|
50
|
+
|
51
|
+
if schema.is_a?(Hash) || schema.is_a?(JsonSchema::Schema)
|
52
|
+
driver = Committee::Drivers::HyperSchema.new
|
53
|
+
|
54
|
+
# The driver itself has its own special cases to be able to parse
|
55
|
+
# either a hash or JsonSchema::Schema object.
|
56
|
+
schema = driver.parse(schema)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Expect the type we want by now. If we don't have it, the user passed
|
60
|
+
# something else non-standard in.
|
61
|
+
if !schema.is_a?(Committee::Drivers::Schema)
|
62
|
+
raise ArgumentError, "Committee: schema expected to be a hash or " \
|
63
|
+
"an instance of Committee::Drivers::Schema."
|
64
|
+
end
|
65
|
+
|
66
|
+
schema
|
67
|
+
end
|
68
|
+
|
69
|
+
def warn_hash_deprecated
|
70
|
+
Committee.warn_deprecated("Committee: passing a hash to schema " \
|
71
|
+
"option is deprecated; please send a driver object instead.")
|
72
|
+
end
|
73
|
+
|
36
74
|
def warn_string_deprecated
|
37
|
-
Committee.warn_deprecated("Committee: passing a string to
|
75
|
+
Committee.warn_deprecated("Committee: passing a string to schema " \
|
76
|
+
"option is deprecated; please send a driver object instead.")
|
38
77
|
end
|
39
78
|
end
|
40
79
|
end
|
@@ -2,27 +2,48 @@ module Committee::Middleware
|
|
2
2
|
class RequestValidation < Base
|
3
3
|
def initialize(app, options={})
|
4
4
|
super
|
5
|
+
|
5
6
|
@allow_form_params = options.fetch(:allow_form_params, true)
|
6
7
|
@allow_query_params = options.fetch(:allow_query_params, true)
|
7
8
|
@check_content_type = options.fetch(:check_content_type, true)
|
8
9
|
@optimistic_json = options.fetch(:optimistic_json, false)
|
9
|
-
@coerce_query_params = options.fetch(:coerce_query_params, false)
|
10
10
|
@strict = options[:strict]
|
11
11
|
|
12
|
+
@coerce_path_params = options.fetch(:coerce_path_params,
|
13
|
+
@schema.driver.default_path_params)
|
14
|
+
@coerce_query_params = options.fetch(:coerce_query_params,
|
15
|
+
@schema.driver.default_query_params)
|
16
|
+
|
12
17
|
# deprecated
|
13
18
|
@allow_extra = options[:allow_extra]
|
14
19
|
end
|
15
20
|
|
16
21
|
def handle(request)
|
17
|
-
link = @router.find_request_link(request)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
link, param_matches = @router.find_request_link(request)
|
23
|
+
path_params = {}
|
24
|
+
|
25
|
+
if link
|
26
|
+
# Attempts to coerce parameters that appear in a link's URL to Ruby
|
27
|
+
# types that can be validated with a schema.
|
28
|
+
if @coerce_path_params
|
29
|
+
path_params = param_matches.merge(
|
30
|
+
Committee::StringParamsCoercer.new(
|
31
|
+
param_matches,
|
32
|
+
link.schema
|
33
|
+
).call
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Attempts to coerce parameters that appear in a query string to Ruby
|
38
|
+
# types that can be validated with a schema.
|
39
|
+
if @coerce_query_params && !request.GET.nil? && !link.schema.nil?
|
40
|
+
request.env["rack.request.query_hash"].merge!(
|
41
|
+
Committee::StringParamsCoercer.new(
|
42
|
+
request.GET,
|
43
|
+
link.schema
|
44
|
+
).call
|
45
|
+
)
|
46
|
+
end
|
26
47
|
end
|
27
48
|
|
28
49
|
request.env[@params_key] = Committee::RequestUnpacker.new(
|
@@ -32,6 +53,8 @@ module Committee::Middleware
|
|
32
53
|
optimistic_json: @optimistic_json
|
33
54
|
).call
|
34
55
|
|
56
|
+
request.env[@params_key].merge!(path_params)
|
57
|
+
|
35
58
|
if link
|
36
59
|
validator = Committee::RequestValidator.new(link, check_content_type: @check_content_type)
|
37
60
|
validator.call(request, request.env[@params_key])
|
@@ -10,7 +10,8 @@ module Committee::Middleware
|
|
10
10
|
def handle(request)
|
11
11
|
status, headers, response = @app.call(request.env)
|
12
12
|
|
13
|
-
|
13
|
+
link, _ = @router.find_request_link(request)
|
14
|
+
if validate?(status) && link
|
14
15
|
full_body = ""
|
15
16
|
response.each do |chunk|
|
16
17
|
full_body << chunk
|
@@ -2,16 +2,27 @@ module Committee::Middleware
|
|
2
2
|
class Stub < Base
|
3
3
|
def initialize(app, options={})
|
4
4
|
super
|
5
|
-
|
6
|
-
|
5
|
+
|
6
|
+
# A bug in Committee's cache implementation meant that it wasn't working
|
7
|
+
# for a very long time, even for people who thought they were taking
|
8
|
+
# advantage of it. I repaired the caching feature, but have disable it by
|
9
|
+
# default so that we don't need to introduce any class-level variables
|
10
|
+
# that could have memory leaking implications. To enable caching, just
|
11
|
+
# pass an empty hash to this option.
|
12
|
+
@cache = options[:cache]
|
13
|
+
|
14
|
+
@call = options[:call]
|
7
15
|
end
|
8
16
|
|
9
17
|
def handle(request)
|
10
|
-
|
18
|
+
link, _ = @router.find_request_link(request)
|
19
|
+
if link
|
11
20
|
headers = { "Content-Type" => "application/json" }
|
12
|
-
|
21
|
+
|
22
|
+
data = cache(link) do
|
13
23
|
Committee::ResponseGenerator.new.call(link)
|
14
24
|
end
|
25
|
+
|
15
26
|
if @call
|
16
27
|
request.env["committee.response"] = data
|
17
28
|
call_status, call_headers, call_body = @app.call(request.env)
|
@@ -29,8 +40,8 @@ module Committee::Middleware
|
|
29
40
|
# will be the same one that we set above)
|
30
41
|
data = request.env["committee.response"]
|
31
42
|
end
|
32
|
-
|
33
|
-
[
|
43
|
+
|
44
|
+
[link.status_success, headers, [JSON.pretty_generate(data)]]
|
34
45
|
else
|
35
46
|
@app.call(request.env)
|
36
47
|
end
|
@@ -38,8 +49,13 @@ module Committee::Middleware
|
|
38
49
|
|
39
50
|
private
|
40
51
|
|
41
|
-
def cache(
|
42
|
-
|
52
|
+
def cache(link)
|
53
|
+
return yield unless @cache
|
54
|
+
|
55
|
+
# Just the object ID is enough to uniquely identify the link, but store
|
56
|
+
# the method and href so that we can more easily introspect the cache if
|
57
|
+
# necessary.
|
58
|
+
key = "#{link.object_id}##{link.method}+#{link.href}"
|
43
59
|
if @cache[key]
|
44
60
|
@cache[key]
|
45
61
|
else
|
@@ -24,7 +24,7 @@ module Committee
|
|
24
24
|
|
25
25
|
def check_content_type!(request, data)
|
26
26
|
content_type = request_media_type(request)
|
27
|
-
if content_type && !empty_request?(request)
|
27
|
+
if content_type && @link.enc_type && !empty_request?(request)
|
28
28
|
unless Rack::Mime.match?(content_type, @link.enc_type)
|
29
29
|
raise Committee::InvalidRequest,
|
30
30
|
%{"Content-Type" request header must be set to "#{@link.enc_type}".}
|
@@ -1,35 +1,80 @@
|
|
1
1
|
module Committee
|
2
2
|
class ResponseGenerator
|
3
3
|
def call(link)
|
4
|
-
data = generate_properties(link
|
4
|
+
data = generate_properties(link, target_schema(link))
|
5
5
|
|
6
|
-
#
|
7
|
-
|
6
|
+
# List is a special case; wrap data in an array.
|
7
|
+
#
|
8
|
+
# This is poor form that's here so as not to introduce breaking behavior.
|
9
|
+
# The "instances" value of "rel" is a Heroku-ism and was originally
|
10
|
+
# introduced before we understood how to use "targetSchema". It's not
|
11
|
+
# meaningful with the context of the hyper-schema specification and
|
12
|
+
# should be eventually be removed.
|
13
|
+
if legacy_hyper_schema_rel?(link)
|
14
|
+
data = [data]
|
15
|
+
end
|
8
16
|
|
9
17
|
data
|
10
18
|
end
|
11
19
|
|
12
20
|
private
|
13
21
|
|
14
|
-
|
22
|
+
# These are basic types that are part of the JSON schema for which we'll
|
23
|
+
# emit zero values when generating a response. For a schema that allows
|
24
|
+
# multiple of the types in the list, types are preferred in the order in
|
25
|
+
# which they're defined.
|
26
|
+
SCALAR_TYPES = {
|
27
|
+
"boolean" => false,
|
28
|
+
"integer" => 0,
|
29
|
+
"number" => 0.0,
|
30
|
+
"string" => "",
|
31
|
+
|
32
|
+
# Prefer null last.
|
33
|
+
"null" => nil,
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
def generate_properties(link, schema)
|
15
37
|
# special example attribute was included; use its value
|
16
|
-
if !schema.data["example"].nil?
|
38
|
+
if schema.data && !schema.data["example"].nil?
|
17
39
|
schema.data["example"]
|
18
|
-
|
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?
|
40
|
+
elsif !schema.all_of.empty? || !schema.properties.empty?
|
24
41
|
data = {}
|
42
|
+
schema.all_of.each do |subschema|
|
43
|
+
data.merge!(generate_properties(link, subschema))
|
44
|
+
end
|
25
45
|
schema.properties.map do |key, value|
|
26
|
-
data[key] = generate_properties(value)
|
46
|
+
data[key] = generate_properties(link, value)
|
27
47
|
end
|
28
48
|
data
|
49
|
+
elsif schema.type.include?("array") && !schema.items.nil?
|
50
|
+
[generate_properties(link, schema.items)]
|
51
|
+
elsif schema.type.any? { |t| SCALAR_TYPES.include?(t) }
|
52
|
+
SCALAR_TYPES.each do |k, v|
|
53
|
+
break(v) if schema.type.include?(k)
|
54
|
+
end
|
29
55
|
else
|
30
|
-
raise(%{At "#{schema.pointer}": no
|
56
|
+
raise(%{At "#{link.method} #{link.href}" "#{schema.pointer}": no } +
|
57
|
+
%{"example" attribute and "null" } +
|
31
58
|
%{is not allowed; don't know how to generate property.})
|
32
59
|
end
|
33
60
|
end
|
61
|
+
|
62
|
+
def legacy_hyper_schema_rel?(link)
|
63
|
+
link.is_a?(Committee::Drivers::HyperSchema::Link) &&
|
64
|
+
link.rel == "instances" &&
|
65
|
+
!link.target_schema
|
66
|
+
end
|
67
|
+
|
68
|
+
# Gets the target schema of a link. This is normally just the standard
|
69
|
+
# response schema, but we allow some legacy behavior for hyper-schema links
|
70
|
+
# tagged with rel=instances to instead use the schema of their parent
|
71
|
+
# resource.
|
72
|
+
def target_schema(link)
|
73
|
+
if link.target_schema
|
74
|
+
link.target_schema
|
75
|
+
elsif legacy_hyper_schema_rel?(link)
|
76
|
+
link.parent
|
77
|
+
end
|
78
|
+
end
|
34
79
|
end
|
35
80
|
end
|
@@ -6,10 +6,7 @@ module Committee
|
|
6
6
|
@link = link
|
7
7
|
@validate_errors = options[:validate_errors]
|
8
8
|
|
9
|
-
|
10
|
-
# ... this is a Herokuism and not in the specification
|
11
|
-
schema = link.target_schema || link.parent
|
12
|
-
@validator = JsonSchema::Validator.new(schema)
|
9
|
+
@validator = JsonSchema::Validator.new(target_schema(link))
|
13
10
|
end
|
14
11
|
|
15
12
|
def self.validate?(status, options = {})
|
@@ -24,7 +21,14 @@ module Committee
|
|
24
21
|
check_content_type!(response)
|
25
22
|
end
|
26
23
|
|
27
|
-
|
24
|
+
# List is a special case; expect data in an array.
|
25
|
+
#
|
26
|
+
# This is poor form that's here so as not to introduce breaking behavior.
|
27
|
+
# The "instances" value of "rel" is a Heroku-ism and was originally
|
28
|
+
# introduced before we understood how to use "targetSchema". It's not
|
29
|
+
# meaningful with the context of the hyper-schema specification and
|
30
|
+
# should be eventually be removed.
|
31
|
+
if legacy_hyper_schema_rel?(@link)
|
28
32
|
if !data.is_a?(Array)
|
29
33
|
raise InvalidResponse, "List endpoints must return an array of objects."
|
30
34
|
end
|
@@ -50,9 +54,29 @@ module Committee
|
|
50
54
|
end
|
51
55
|
|
52
56
|
def check_content_type!(response)
|
53
|
-
|
54
|
-
|
55
|
-
|
57
|
+
if @link.media_type
|
58
|
+
unless Rack::Mime.match?(response_media_type(response), @link.media_type)
|
59
|
+
raise Committee::InvalidResponse,
|
60
|
+
%{"Content-Type" response header must be set to "#{@link.media_type}".}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def legacy_hyper_schema_rel?(link)
|
66
|
+
link.is_a?(Committee::Drivers::HyperSchema::Link) &&
|
67
|
+
link.rel == "instances" &&
|
68
|
+
!link.target_schema
|
69
|
+
end
|
70
|
+
|
71
|
+
# Gets the target schema of a link. This is normally just the standard
|
72
|
+
# response schema, but we allow some legacy behavior for hyper-schema links
|
73
|
+
# tagged with rel=instances to instead use the schema of their parent
|
74
|
+
# resource.
|
75
|
+
def target_schema(link)
|
76
|
+
if link.target_schema
|
77
|
+
link.target_schema
|
78
|
+
elsif legacy_hyper_schema_rel?(link)
|
79
|
+
link.parent
|
56
80
|
end
|
57
81
|
end
|
58
82
|
end
|
data/lib/committee/router.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Committee
|
2
2
|
class Router
|
3
3
|
def initialize(schema, options = {})
|
4
|
-
@routes = build_routes(schema)
|
5
4
|
@prefix = options[:prefix]
|
6
5
|
@prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
|
6
|
+
@schema = schema
|
7
7
|
end
|
8
8
|
|
9
9
|
def includes?(path)
|
@@ -16,10 +16,11 @@ module Committee
|
|
16
16
|
|
17
17
|
def find_link(method, path)
|
18
18
|
path = path.gsub(@prefix_regexp, "") if @prefix
|
19
|
-
if method_routes = @routes[method]
|
19
|
+
if method_routes = @schema.routes[method]
|
20
20
|
method_routes.each do |pattern, link|
|
21
|
-
if
|
22
|
-
|
21
|
+
if matches = pattern.match(path)
|
22
|
+
hash = Hash[matches.names.zip(matches.captures)]
|
23
|
+
return link, hash
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
@@ -29,34 +30,5 @@ module Committee
|
|
29
30
|
def find_request_link(request)
|
30
31
|
find_link(request.request_method, request.path_info)
|
31
32
|
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
33
|
end
|
62
34
|
end
|
@@ -1,10 +1,15 @@
|
|
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
1
|
module Committee
|
7
|
-
|
2
|
+
# StringParamsCoercer takes parameters that are specified over a medium that
|
3
|
+
# can only accept strings (for example in a URL path or in query parameters)
|
4
|
+
# and attempts to coerce them into known types based of a link's schema
|
5
|
+
# definition.
|
6
|
+
#
|
7
|
+
# Currently supported types: null, integer, number and boolean.
|
8
|
+
#
|
9
|
+
# +call+ returns a hash of all params which could be coerced - coercion
|
10
|
+
# errors are simply ignored and expected to be handled later by schema
|
11
|
+
# validation.
|
12
|
+
class StringParamsCoercer
|
8
13
|
def initialize(query_hash, schema)
|
9
14
|
@query_hash = query_hash
|
10
15
|
@schema = schema
|
@@ -1,19 +1,42 @@
|
|
1
1
|
module Committee::Test
|
2
2
|
module Methods
|
3
3
|
def assert_schema_conform
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
@committee_schema ||= begin
|
5
|
+
# The preferred option. The user has already parsed a schema elsewhere
|
6
|
+
# and we therefore don't have to worry about any performance
|
7
|
+
# implications of having to do it for every single test suite.
|
8
|
+
if committee_schema
|
9
|
+
committee_schema
|
10
|
+
else
|
11
|
+
schema = schema_contents
|
12
|
+
|
13
|
+
if schema.is_a?(String)
|
14
|
+
warn_string_deprecated
|
15
|
+
elsif schema.is_a?(Hash)
|
16
|
+
warn_hash_deprecated
|
17
|
+
end
|
18
|
+
|
19
|
+
if schema.is_a?(String)
|
20
|
+
schema = JSON.parse(schema)
|
21
|
+
end
|
8
22
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
23
|
+
if schema.is_a?(Hash) || schema.is_a?(JsonSchema::Schema)
|
24
|
+
driver = Committee::Drivers::HyperSchema.new
|
25
|
+
|
26
|
+
# The driver itself has its own special cases to be able to parse
|
27
|
+
# either a hash or JsonSchema::Schema object.
|
28
|
+
schema = driver.parse(schema)
|
29
|
+
end
|
30
|
+
|
31
|
+
schema
|
32
|
+
end
|
13
33
|
end
|
14
|
-
@router ||= Committee::Router.new(@schema, prefix: schema_url_prefix)
|
15
34
|
|
16
|
-
|
35
|
+
@committee_router ||= Committee::Router.new(@committee_schema,
|
36
|
+
prefix: schema_url_prefix)
|
37
|
+
|
38
|
+
link, _ = @committee_router.find_request_link(last_request)
|
39
|
+
unless link
|
17
40
|
response = "`#{last_request.request_method} #{last_request.path_info}` undefined in schema."
|
18
41
|
raise Committee::InvalidResponse.new(response)
|
19
42
|
end
|
@@ -28,6 +51,12 @@ module Committee::Test
|
|
28
51
|
Committee.warn_deprecated("Committee: use of #assert_schema_content_type is deprecated; use #assert_schema_conform instead.")
|
29
52
|
end
|
30
53
|
|
54
|
+
# Can be overridden with a different driver name for other API definition
|
55
|
+
# formats.
|
56
|
+
def committee_schema
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
31
60
|
# can be overridden alternatively to #schema_path in case the schema is
|
32
61
|
# easier to access as a string
|
33
62
|
# blob
|
@@ -36,15 +65,23 @@ module Committee::Test
|
|
36
65
|
end
|
37
66
|
|
38
67
|
def schema_path
|
39
|
-
raise "Please override #
|
68
|
+
raise "Please override #commitee_schema."
|
40
69
|
end
|
41
70
|
|
42
71
|
def schema_url_prefix
|
43
72
|
nil
|
44
73
|
end
|
45
74
|
|
75
|
+
def warn_hash_deprecated
|
76
|
+
Committee.warn_deprecated("Committee: returning a hash from " \
|
77
|
+
"#schema_contents and using #schema_path is deprecated; please " \
|
78
|
+
"override #committee_schema instead.")
|
79
|
+
end
|
80
|
+
|
46
81
|
def warn_string_deprecated
|
47
|
-
Committee.warn_deprecated("Committee: returning a string from
|
82
|
+
Committee.warn_deprecated("Committee: returning a string from " \
|
83
|
+
"#schema_contents is deprecated; please override #committee_schema " \
|
84
|
+
"instead.")
|
48
85
|
end
|
49
86
|
|
50
87
|
def validate_response?(status)
|
data/lib/committee.rb
CHANGED
@@ -3,7 +3,7 @@ require "json_schema"
|
|
3
3
|
require "rack"
|
4
4
|
|
5
5
|
require_relative "committee/errors"
|
6
|
-
require_relative "committee/
|
6
|
+
require_relative "committee/string_params_coercer"
|
7
7
|
require_relative "committee/request_unpacker"
|
8
8
|
require_relative "committee/request_validator"
|
9
9
|
require_relative "committee/response_generator"
|
@@ -11,14 +11,28 @@ require_relative "committee/response_validator"
|
|
11
11
|
require_relative "committee/router"
|
12
12
|
require_relative "committee/validation_error"
|
13
13
|
|
14
|
+
require_relative "committee/drivers"
|
15
|
+
require_relative "committee/drivers/hyper_schema"
|
16
|
+
require_relative "committee/drivers/open_api_2"
|
17
|
+
|
14
18
|
require_relative "committee/middleware/base"
|
15
19
|
require_relative "committee/middleware/request_validation"
|
16
20
|
require_relative "committee/middleware/response_validation"
|
17
21
|
require_relative "committee/middleware/stub"
|
18
22
|
|
23
|
+
require_relative "committee/bin/committee_stub"
|
24
|
+
|
19
25
|
require_relative "committee/test/methods"
|
20
26
|
|
21
27
|
module Committee
|
28
|
+
def self.debug?
|
29
|
+
ENV["COMMITTEE_DEBUG"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log_debug(message)
|
33
|
+
$stderr.puts(message) if debug?
|
34
|
+
end
|
35
|
+
|
22
36
|
def self.warn_deprecated(message)
|
23
37
|
if !$VERBOSE.nil?
|
24
38
|
$stderr.puts(message)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
describe Committee::Bin::CommitteeStub do
|
4
|
+
before do
|
5
|
+
@bin = Committee::Bin::CommitteeStub.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "produces a Rack app" do
|
9
|
+
app = @bin.get_app(hyper_schema, {})
|
10
|
+
assert_kind_of Rack::Builder, app
|
11
|
+
end
|
12
|
+
|
13
|
+
it "parses command line options" do
|
14
|
+
options, parser = @bin.get_options_parser
|
15
|
+
|
16
|
+
parser.parse!(["--help"])
|
17
|
+
assert_equal true, options[:help]
|
18
|
+
|
19
|
+
parser.parse!([
|
20
|
+
"--driver", "open_api_2",
|
21
|
+
"--tolerant", "true",
|
22
|
+
"--port", "1234"
|
23
|
+
])
|
24
|
+
assert_equal :open_api_2, options[:driver]
|
25
|
+
assert_equal true, options[:tolerant]
|
26
|
+
assert_equal "1234", options[:port]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Committee::Bin::CommitteeStub, "app" do
|
31
|
+
include Rack::Test::Methods
|
32
|
+
|
33
|
+
before do
|
34
|
+
@bin = Committee::Bin::CommitteeStub.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def app
|
38
|
+
@bin.get_app(hyper_schema, {})
|
39
|
+
end
|
40
|
+
|
41
|
+
it "defaults to a 404" do
|
42
|
+
get "/foos"
|
43
|
+
assert_equal 404, last_response.status
|
44
|
+
end
|
45
|
+
end
|
data/test/bin_test.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
#
|
4
|
+
# The purpose of this sets of tests is just to include our Ruby executables
|
5
|
+
# where possible so that we can get very basic sanity checks on their syntax
|
6
|
+
# (which is something that of course Ruby can't do by default).
|
7
|
+
#
|
8
|
+
# We can do this without actually executing them because they're gated by `if
|
9
|
+
# $0 == __FILE__` statements.
|
10
|
+
#
|
11
|
+
|
12
|
+
describe "executables in bin/" do
|
13
|
+
before do
|
14
|
+
@bin_dir = File.expand_path("../../bin", __FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has roughly valid Ruby structure for committee-stub" do
|
18
|
+
load File.join(@bin_dir, "committee-stub")
|
19
|
+
end
|
20
|
+
end
|