committee 1.15.0 → 2.0.0.pre
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.
- 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
|