committee 2.5.1 → 3.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/committee.rb +16 -5
- data/lib/committee/bin/committee_stub.rb +4 -0
- data/lib/committee/drivers.rb +12 -29
- data/lib/committee/drivers/hyper_schema.rb +9 -0
- data/lib/committee/drivers/open_api_2.rb +9 -20
- data/lib/committee/drivers/open_api_3.rb +70 -0
- data/lib/committee/errors.rb +3 -0
- data/lib/committee/middleware/base.rb +20 -62
- data/lib/committee/middleware/request_validation.rb +5 -62
- data/lib/committee/middleware/response_validation.rb +5 -19
- data/lib/committee/middleware/stub.rb +5 -1
- data/lib/committee/parameter_coercer.rb +1 -0
- data/lib/committee/request_unpacker.rb +2 -5
- data/lib/committee/schema_validator/hyper_schema.rb +92 -0
- data/lib/committee/{request_validator.rb → schema_validator/hyper_schema/request_validator.rb} +1 -1
- data/lib/committee/{response_generator.rb → schema_validator/hyper_schema/response_generator.rb} +1 -1
- data/lib/committee/{response_validator.rb → schema_validator/hyper_schema/response_validator.rb} +6 -6
- data/lib/committee/{router.rb → schema_validator/hyper_schema/router.rb} +10 -4
- data/lib/committee/{string_params_coercer.rb → schema_validator/hyper_schema/string_params_coercer.rb} +1 -1
- data/lib/committee/schema_validator/open_api_3.rb +67 -0
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +98 -0
- data/lib/committee/schema_validator/open_api_3/request_validator.rb +17 -0
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +35 -0
- data/lib/committee/schema_validator/open_api_3/router.rb +26 -0
- data/lib/committee/schema_validator/option.rb +31 -0
- data/lib/committee/test/methods.rb +14 -117
- data/test/bin/committee_stub_test.rb +6 -0
- data/test/drivers/open_api_2_test.rb +0 -81
- data/test/drivers/open_api_3_test.rb +81 -0
- data/test/drivers_test.rb +2 -42
- data/test/middleware/base_test.rb +42 -21
- data/test/middleware/request_validation_open_api_3_test.rb +499 -0
- data/test/middleware/request_validation_test.rb +18 -0
- data/test/middleware/response_validation_open_api_3_test.rb +96 -0
- data/test/middleware/response_validation_test.rb +7 -30
- data/test/middleware/stub_test.rb +9 -0
- data/test/request_unpacker_test.rb +55 -21
- data/test/{request_validator_test.rb → schema_validator/hyper_schema/request_validator_test.rb} +4 -4
- data/test/{response_generator_test.rb → schema_validator/hyper_schema/response_generator_test.rb} +11 -11
- data/test/{response_validator_test.rb → schema_validator/hyper_schema/response_validator_test.rb} +3 -3
- data/test/{router_test.rb → schema_validator/hyper_schema/router_test.rb} +8 -10
- data/test/{string_params_coercer_test.rb → schema_validator/hyper_schema/string_params_coercer_test.rb} +3 -3
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +132 -0
- data/test/schema_validator/open_api_3/request_validator_test.rb +151 -0
- data/test/schema_validator/open_api_3/response_validator_test.rb +55 -0
- data/test/test/methods_new_version_test.rb +11 -20
- data/test/test/methods_test.rb +51 -55
- data/test/test_helper.rb +22 -8
- metadata +46 -18
@@ -1,32 +1,18 @@
|
|
1
1
|
module Committee::Middleware
|
2
2
|
class ResponseValidation < Base
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :validate_errors
|
4
4
|
|
5
5
|
def initialize(app, options = {})
|
6
6
|
super
|
7
|
-
@
|
8
|
-
|
9
|
-
unless options[:validate_errors].nil?
|
10
|
-
@validate_success_only = !options[:validate_errors]
|
11
|
-
Committee.warn_deprecated("Committee: validate_errors option is deprecated; " \
|
12
|
-
"please use validate_success_only=#{@validate_success_only}.")
|
13
|
-
end
|
14
|
-
|
7
|
+
@validate_errors = options[:validate_errors]
|
15
8
|
@error_handler = options[:error_handler]
|
16
9
|
end
|
17
10
|
|
18
11
|
def handle(request)
|
19
12
|
status, headers, response = @app.call(request.env)
|
20
13
|
|
21
|
-
|
22
|
-
if validate?(status)
|
23
|
-
full_body = ""
|
24
|
-
response.each do |chunk|
|
25
|
-
full_body << chunk
|
26
|
-
end
|
27
|
-
data = full_body.empty? ? {} : JSON.parse(full_body)
|
28
|
-
Committee::ResponseValidator.new(link, validate_success_only: validate_success_only).call(status, headers, data)
|
29
|
-
end
|
14
|
+
v = build_schema_validator(request)
|
15
|
+
v.response_validate(status, headers, response) if v.link_exist? && validate?(status)
|
30
16
|
|
31
17
|
[status, headers, response]
|
32
18
|
rescue Committee::InvalidResponse
|
@@ -40,7 +26,7 @@ module Committee::Middleware
|
|
40
26
|
end
|
41
27
|
|
42
28
|
def validate?(status)
|
43
|
-
|
29
|
+
status != 204 and validate_errors || (200...300).include?(status)
|
44
30
|
end
|
45
31
|
end
|
46
32
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# Don't Support OpenAPI3
|
2
|
+
|
1
3
|
module Committee::Middleware
|
2
4
|
class Stub < Base
|
3
5
|
def initialize(app, options={})
|
@@ -12,6 +14,8 @@ module Committee::Middleware
|
|
12
14
|
@cache = options[:cache]
|
13
15
|
|
14
16
|
@call = options[:call]
|
17
|
+
|
18
|
+
raise Committee::NotSupportOpenAPI3.new("OpenAPI3 not support stub") unless @schema.support_stub?
|
15
19
|
end
|
16
20
|
|
17
21
|
def handle(request)
|
@@ -20,7 +24,7 @@ module Committee::Middleware
|
|
20
24
|
headers = { "Content-Type" => "application/json" }
|
21
25
|
|
22
26
|
data, schema = cache(link) do
|
23
|
-
Committee::ResponseGenerator.new.call(link)
|
27
|
+
Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
|
24
28
|
end
|
25
29
|
|
26
30
|
if @call
|
@@ -7,8 +7,7 @@ module Committee
|
|
7
7
|
@allow_query_params = options[:allow_query_params]
|
8
8
|
@coerce_form_params = options[:coerce_form_params]
|
9
9
|
@optimistic_json = options[:optimistic_json]
|
10
|
-
@
|
11
|
-
@schema = options[:schema]
|
10
|
+
@schema_validator = options[:schema_validator]
|
12
11
|
end
|
13
12
|
|
14
13
|
def call
|
@@ -31,9 +30,7 @@ module Committee
|
|
31
30
|
# PUT or PATCH too. Silly Rack.
|
32
31
|
p = @request.POST
|
33
32
|
|
34
|
-
|
35
|
-
Committee::StringParamsCoercer.new(p, @schema, coerce_recursive: @coerce_recursive).call!
|
36
|
-
end
|
33
|
+
@schema_validator.coerce_form_params(p) if @coerce_form_params
|
37
34
|
|
38
35
|
p
|
39
36
|
else
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class Committee::SchemaValidator
|
2
|
+
class HyperSchema
|
3
|
+
attr_reader :link, :param_matches, :validator_option
|
4
|
+
|
5
|
+
def initialize(router, request, validator_option)
|
6
|
+
@link, @param_matches = router.find_request_link(request)
|
7
|
+
@validator_option = validator_option
|
8
|
+
end
|
9
|
+
|
10
|
+
def request_validate(request)
|
11
|
+
# Attempts to coerce parameters that appear in a link's URL to Ruby
|
12
|
+
# types that can be validated with a schema.
|
13
|
+
param_matches_hash = validator_option.coerce_path_params ? coerce_path_params : {}
|
14
|
+
|
15
|
+
# Attempts to coerce parameters that appear in a query string to Ruby
|
16
|
+
# types that can be validated with a schema.
|
17
|
+
coerce_query_params(request) if validator_option.coerce_query_params
|
18
|
+
|
19
|
+
request_unpack(request)
|
20
|
+
|
21
|
+
request.env[validator_option.params_key].merge!(param_matches_hash) if param_matches_hash
|
22
|
+
|
23
|
+
request_schema_validation(request)
|
24
|
+
parameter_coerce!(request, link, validator_option.params_key)
|
25
|
+
parameter_coerce!(request, link, "rack.request.query_hash") if link_exist? && !request.GET.nil? && !link.schema.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_validate(status, headers, response)
|
29
|
+
return unless link_exist?
|
30
|
+
|
31
|
+
full_body = ""
|
32
|
+
response.each do |chunk|
|
33
|
+
full_body << chunk
|
34
|
+
end
|
35
|
+
data = JSON.parse(full_body)
|
36
|
+
Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_errors: validator_option.validate_errors).call(status, headers, data)
|
37
|
+
end
|
38
|
+
|
39
|
+
def link_exist?
|
40
|
+
!link.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def coerce_form_params(parameter)
|
44
|
+
return unless link.schema
|
45
|
+
Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(parameter, link.schema).call!
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def coerce_path_params
|
51
|
+
return unless link_exist?
|
52
|
+
|
53
|
+
Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(param_matches, link.schema, coerce_recursive: validator_option.coerce_recursive).call!
|
54
|
+
param_matches
|
55
|
+
end
|
56
|
+
|
57
|
+
def coerce_query_params(request)
|
58
|
+
return unless link_exist?
|
59
|
+
return if request.GET.nil? || link.schema.nil?
|
60
|
+
|
61
|
+
Committee::SchemaValidator::HyperSchema::StringParamsCoercer.new(request.GET, link.schema, coerce_recursive: validator_option.coerce_recursive).call!
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_unpack(request)
|
65
|
+
request.env[validator_option.params_key], request.env[validator_option.headers_key] = Committee::RequestUnpacker.new(
|
66
|
+
request,
|
67
|
+
allow_form_params: validator_option.allow_form_params,
|
68
|
+
allow_query_params: validator_option.allow_query_params,
|
69
|
+
coerce_form_params: validator_option.coerce_form_params,
|
70
|
+
optimistic_json: validator_option.optimistic_json,
|
71
|
+
schema_validator: self
|
72
|
+
).call
|
73
|
+
end
|
74
|
+
|
75
|
+
def request_schema_validation(request)
|
76
|
+
return unless link_exist?
|
77
|
+
validator = Committee::SchemaValidator::HyperSchema::RequestValidator.new(link, check_content_type: validator_option.check_content_type, check_header: validator_option.check_header)
|
78
|
+
validator.call(request, request.env[validator_option.params_key], request.env[validator_option.headers_key])
|
79
|
+
end
|
80
|
+
|
81
|
+
def parameter_coerce!(request, link, coerce_key)
|
82
|
+
return unless link_exist?
|
83
|
+
|
84
|
+
Committee::ParameterCoercer.
|
85
|
+
new(request.env[coerce_key],
|
86
|
+
link.schema,
|
87
|
+
coerce_date_times: validator_option.coerce_date_times,
|
88
|
+
coerce_recursive: validator_option.coerce_recursive).
|
89
|
+
call!
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/committee/{response_validator.rb → schema_validator/hyper_schema/response_validator.rb}
RENAMED
@@ -1,18 +1,18 @@
|
|
1
1
|
module Committee
|
2
|
-
class ResponseValidator
|
3
|
-
attr_reader :
|
2
|
+
class SchemaValidator::HyperSchema::ResponseValidator
|
3
|
+
attr_reader :validate_errors
|
4
4
|
|
5
5
|
def initialize(link, options = {})
|
6
6
|
@link = link
|
7
|
-
@
|
7
|
+
@validate_errors = options[:validate_errors]
|
8
8
|
|
9
9
|
@validator = JsonSchema::Validator.new(target_schema(link))
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.validate?(status, options = {})
|
13
|
-
|
13
|
+
validate_errors = options[:validate_errors]
|
14
14
|
|
15
|
-
status != 204 and
|
15
|
+
status != 204 and validate_errors || (200...300).include?(status)
|
16
16
|
end
|
17
17
|
|
18
18
|
def call(status, headers, data)
|
@@ -41,7 +41,7 @@ module Committee
|
|
41
41
|
return if data == nil
|
42
42
|
end
|
43
43
|
|
44
|
-
if self.class.validate?(status,
|
44
|
+
if self.class.validate?(status, validate_errors: validate_errors) && !@validator.validate(data)
|
45
45
|
errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
|
46
46
|
raise InvalidResponse, "Invalid response.\n\n#{errors}"
|
47
47
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module Committee
|
2
|
-
class Router
|
3
|
-
def initialize(schema,
|
4
|
-
@prefix =
|
2
|
+
class SchemaValidator::HyperSchema::Router
|
3
|
+
def initialize(schema, validator_option)
|
4
|
+
@prefix = validator_option.prefix
|
5
5
|
@prefix_regexp = /\A#{Regexp.escape(@prefix)}/.freeze if @prefix
|
6
6
|
@schema = schema
|
7
|
+
|
8
|
+
@validator_option = validator_option
|
7
9
|
end
|
8
10
|
|
9
11
|
def includes?(path)
|
@@ -23,12 +25,16 @@ module Committee
|
|
23
25
|
else
|
24
26
|
nil
|
25
27
|
end
|
26
|
-
end.compact.
|
28
|
+
end.compact.sort.first
|
27
29
|
link_with_matches.nil? ? nil : link_with_matches.slice(1, 2)
|
28
30
|
end
|
29
31
|
|
30
32
|
def find_request_link(request)
|
31
33
|
find_link(request.request_method, request.path_info)
|
32
34
|
end
|
35
|
+
|
36
|
+
def build_schema_validator(request)
|
37
|
+
Committee::SchemaValidator::HyperSchema.new(self, request, @validator_option)
|
38
|
+
end
|
33
39
|
end
|
34
40
|
end
|
@@ -9,7 +9,7 @@ module Committee
|
|
9
9
|
# +call+ returns a hash of all params which could be coerced - coercion
|
10
10
|
# errors are simply ignored and expected to be handled later by schema
|
11
11
|
# validation.
|
12
|
-
class StringParamsCoercer
|
12
|
+
class SchemaValidator::HyperSchema::StringParamsCoercer
|
13
13
|
def initialize(query_hash, schema, options = {})
|
14
14
|
@query_hash = query_hash
|
15
15
|
@schema = schema
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class Committee::SchemaValidator
|
2
|
+
class OpenAPI3
|
3
|
+
def initialize(router, request, validator_option)
|
4
|
+
@router = router
|
5
|
+
@request = request
|
6
|
+
@operation_object = router.operation_object(request)
|
7
|
+
@validator_option = validator_option
|
8
|
+
end
|
9
|
+
|
10
|
+
def request_validate(request)
|
11
|
+
path_params = validator_option.coerce_path_params ? coerce_path_params : {}
|
12
|
+
# coerce_query_params(request) if validator_option.coerce_query_params
|
13
|
+
|
14
|
+
request_unpack(request)
|
15
|
+
|
16
|
+
request.env[validator_option.params_key]&.merge!(path_params) unless path_params.empty?
|
17
|
+
|
18
|
+
request_schema_validation(request)
|
19
|
+
|
20
|
+
@operation_object&.coerce_request_parameter(request.env["rack.request.query_hash"], validator_option) if !request.GET.nil? && !request.env["rack.request.query_hash"].empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def response_validate(status, headers, response)
|
24
|
+
full_body = ""
|
25
|
+
response.each do |chunk|
|
26
|
+
full_body << chunk
|
27
|
+
end
|
28
|
+
data = JSON.parse(full_body)
|
29
|
+
Committee::SchemaValidator::OpenAPI3::ResponseValidator.new(@operation_object, validator_option).call(status, headers, data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def link_exist?
|
33
|
+
!@operation_object.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def coerce_form_params(_parameter)
|
37
|
+
# nothing because when request_schema_validation, check and coerce
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :validator_option
|
43
|
+
|
44
|
+
def coerce_path_params
|
45
|
+
return {} unless link_exist?
|
46
|
+
@operation_object.coerce_path_parameter(@validator_option)
|
47
|
+
end
|
48
|
+
|
49
|
+
def request_schema_validation(request)
|
50
|
+
return unless @operation_object
|
51
|
+
|
52
|
+
validator = Committee::SchemaValidator::OpenAPI3::RequestValidator.new(@operation_object, validator_option: validator_option)
|
53
|
+
validator.call(request, request.env[validator_option.params_key], request.env[validator_option.headers_key])
|
54
|
+
end
|
55
|
+
|
56
|
+
def request_unpack(request)
|
57
|
+
request.env[validator_option.params_key], request.env[validator_option.headers_key] = Committee::RequestUnpacker.new(
|
58
|
+
request,
|
59
|
+
allow_form_params: validator_option.allow_form_params,
|
60
|
+
allow_query_params: validator_option.allow_query_params,
|
61
|
+
coerce_form_params: validator_option.coerce_form_params,
|
62
|
+
optimistic_json: validator_option.optimistic_json,
|
63
|
+
schema_validator: self
|
64
|
+
).call
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Committee
|
2
|
+
class SchemaValidator::OpenAPI3::OperationWrapper
|
3
|
+
# # @param request_operation [OpenAPIParser::RequestOperation]
|
4
|
+
def initialize(request_operation)
|
5
|
+
@request_operation = request_operation
|
6
|
+
end
|
7
|
+
|
8
|
+
def path_params
|
9
|
+
request_operation.path_params
|
10
|
+
end
|
11
|
+
|
12
|
+
def original_path
|
13
|
+
request_operation.original_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def coerce_path_parameter(validator_option)
|
17
|
+
options = build_openapi_parser_path_option(validator_option)
|
18
|
+
return {} unless options.coerce_value
|
19
|
+
|
20
|
+
request_operation.validate_path_params(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def coerce_request_parameter(params, validator_option)
|
24
|
+
options = build_openapi_parser_get_option(validator_option)
|
25
|
+
return unless options.coerce_value
|
26
|
+
|
27
|
+
request_operation.validate_request_parameter(params, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_response_params(status_code, content_type, params)
|
31
|
+
return request_operation.validate_response_body(status_code, content_type, params)
|
32
|
+
rescue OpenAPIParser::OpenAPIError => e
|
33
|
+
raise Committee::InvalidRequest.new(e.message)
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_request_params(params, validator_option)
|
37
|
+
ret, err = case request_operation.http_method
|
38
|
+
when 'get'
|
39
|
+
validate_get_request_params(params, validator_option)
|
40
|
+
when 'post'
|
41
|
+
validate_post_request_params(params, validator_option)
|
42
|
+
when 'put'
|
43
|
+
validate_post_request_params(params, validator_option)
|
44
|
+
when 'patch'
|
45
|
+
validate_post_request_params(params, validator_option)
|
46
|
+
when 'delete'
|
47
|
+
validate_get_request_params(params, validator_option)
|
48
|
+
else
|
49
|
+
raise "Committee OpenAPI3 not support #{request_operation.http_method} method"
|
50
|
+
end
|
51
|
+
raise err if err
|
52
|
+
ret
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_reader :request_operation
|
58
|
+
|
59
|
+
# @!attribute [r] request_operation
|
60
|
+
# @return [OpenAPIParser::RequestOperation]
|
61
|
+
|
62
|
+
# @return [OpenAPIParser::SchemaValidator::Options]
|
63
|
+
def build_openapi_parser_path_option(validator_option)
|
64
|
+
coerce_value = validator_option.coerce_path_params
|
65
|
+
datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
|
66
|
+
OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,datetime_coerce_class: datetime_coerce_class)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [OpenAPIParser::SchemaValidator::Options]
|
70
|
+
def build_openapi_parser_post_option(validator_option)
|
71
|
+
coerce_value = validator_option.coerce_form_params
|
72
|
+
datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
|
73
|
+
OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,datetime_coerce_class: datetime_coerce_class)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [OpenAPIParser::SchemaValidator::Options]
|
77
|
+
def build_openapi_parser_get_option(validator_option)
|
78
|
+
coerce_value = validator_option.coerce_query_params
|
79
|
+
datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
|
80
|
+
OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,datetime_coerce_class: datetime_coerce_class)
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_get_request_params(params, validator_option)
|
84
|
+
# bad performance because when we coerce value, same check
|
85
|
+
request_operation.validate_request_parameter(params, build_openapi_parser_get_option(validator_option))
|
86
|
+
rescue OpenAPIParser::OpenAPIError => e
|
87
|
+
raise Committee::InvalidRequest.new(e.message)
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_post_request_params(params, validator_option)
|
91
|
+
# bad performance because when we coerce value, same check
|
92
|
+
# TODO: support other content type
|
93
|
+
return request_operation.validate_request_body('application/json', params, build_openapi_parser_post_option(validator_option))
|
94
|
+
rescue => e
|
95
|
+
raise Committee::InvalidRequest.new(e.message)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|