committee_firetail 5.0.0
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 +7 -0
- data/bin/committee-stub +23 -0
- data/lib/committee/bin/committee_stub.rb +67 -0
- data/lib/committee/drivers/driver.rb +47 -0
- data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
- data/lib/committee/drivers/hyper_schema/link.rb +68 -0
- data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
- data/lib/committee/drivers/hyper_schema.rb +12 -0
- data/lib/committee/drivers/open_api_2/driver.rb +252 -0
- data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2/link.rb +36 -0
- data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
- data/lib/committee/drivers/open_api_2/schema.rb +26 -0
- data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2.rb +13 -0
- data/lib/committee/drivers/open_api_3/driver.rb +51 -0
- data/lib/committee/drivers/open_api_3/schema.rb +41 -0
- data/lib/committee/drivers/open_api_3.rb +11 -0
- data/lib/committee/drivers/schema.rb +23 -0
- data/lib/committee/drivers.rb +84 -0
- data/lib/committee/errors.rb +36 -0
- data/lib/committee/middleware/base.rb +57 -0
- data/lib/committee/middleware/request_validation.rb +41 -0
- data/lib/committee/middleware/response_validation.rb +58 -0
- data/lib/committee/middleware/stub.rb +75 -0
- data/lib/committee/middleware.rb +11 -0
- data/lib/committee/request_unpacker.rb +91 -0
- data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
- data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
- data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
- data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
- data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
- data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
- data/lib/committee/schema_validator/hyper_schema.rb +119 -0
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
- data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
- data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
- data/lib/committee/schema_validator/open_api_3.rb +120 -0
- data/lib/committee/schema_validator/option.rb +60 -0
- data/lib/committee/schema_validator.rb +23 -0
- data/lib/committee/test/methods.rb +84 -0
- data/lib/committee/test/schema_coverage.rb +101 -0
- data/lib/committee/utils.rb +28 -0
- data/lib/committee/validation_error.rb +26 -0
- data/lib/committee/version.rb +5 -0
- data/lib/committee.rb +40 -0
- data/test/bin/committee_stub_test.rb +57 -0
- data/test/bin_test.rb +25 -0
- data/test/committee_test.rb +77 -0
- data/test/drivers/hyper_schema/driver_test.rb +49 -0
- data/test/drivers/hyper_schema/link_test.rb +56 -0
- data/test/drivers/open_api_2/driver_test.rb +156 -0
- data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
- data/test/drivers/open_api_2/link_test.rb +52 -0
- data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
- data/test/drivers/open_api_3/driver_test.rb +84 -0
- data/test/drivers_test.rb +154 -0
- data/test/middleware/base_test.rb +130 -0
- data/test/middleware/request_validation_open_api_3_test.rb +626 -0
- data/test/middleware/request_validation_test.rb +516 -0
- data/test/middleware/response_validation_open_api_3_test.rb +291 -0
- data/test/middleware/response_validation_test.rb +189 -0
- data/test/middleware/stub_test.rb +145 -0
- data/test/request_unpacker_test.rb +200 -0
- data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
- data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
- data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
- data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
- data/test/schema_validator/hyper_schema/router_test.rb +88 -0
- data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
- data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
- data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
- data/test/test/methods_new_version_test.rb +97 -0
- data/test/test/methods_test.rb +363 -0
- data/test/test/schema_coverage_test.rb +216 -0
- data/test/test_helper.rb +120 -0
- data/test/validation_error_test.rb +25 -0
- metadata +328 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI2
|
6
|
+
# ParameterSchemaBuilder converts OpenAPI 2 link parameters, which are not
|
7
|
+
# quite JSON schemas (but will be in OpenAPI 3) into synthetic schemas that
|
8
|
+
# we can use to do some basic request validation.
|
9
|
+
class ParameterSchemaBuilder < SchemaBuilder
|
10
|
+
# Returns a tuple of (schema, schema_data) where only one of the two
|
11
|
+
# values is present. This is either a full schema that's ready to go _or_
|
12
|
+
# a hash of unparsed schema data.
|
13
|
+
def call
|
14
|
+
if link_data["parameters"]
|
15
|
+
body_param = link_data["parameters"].detect { |p| p["in"] == "body" }
|
16
|
+
if body_param
|
17
|
+
check_required_fields!(body_param)
|
18
|
+
|
19
|
+
if link_data["parameters"].detect { |p| p["in"] == "form" } != nil
|
20
|
+
raise ArgumentError, "Committee: can't mix body parameter " \
|
21
|
+
"with form parameters."
|
22
|
+
end
|
23
|
+
|
24
|
+
schema_data = body_param["schema"]
|
25
|
+
[nil, schema_data]
|
26
|
+
else
|
27
|
+
link_schema = JsonSchema::Schema.new
|
28
|
+
link_schema.properties = {}
|
29
|
+
link_schema.required = []
|
30
|
+
|
31
|
+
parameters = link_data["parameters"].reject { |param_data| param_data["in"] == "header" }
|
32
|
+
parameters.each do |param_data|
|
33
|
+
check_required_fields!(param_data)
|
34
|
+
|
35
|
+
param_schema = JsonSchema::Schema.new
|
36
|
+
|
37
|
+
# We could probably use more validation here, but the formats of
|
38
|
+
# OpenAPI 2 are based off of what's available in JSON schema, and
|
39
|
+
# therefore this should map over quite well.
|
40
|
+
param_schema.type = [param_data["type"]]
|
41
|
+
|
42
|
+
param_schema.enum = param_data["enum"] unless param_data["enum"].nil?
|
43
|
+
|
44
|
+
# validation: string
|
45
|
+
param_schema.format = param_data["format"] unless param_data["format"].nil?
|
46
|
+
param_schema.pattern = Regexp.new(param_data["pattern"]) unless param_data["pattern"].nil?
|
47
|
+
param_schema.min_length = param_data["minLength"] unless param_data["minLength"].nil?
|
48
|
+
param_schema.max_length = param_data["maxLength"] unless param_data["maxLength"].nil?
|
49
|
+
|
50
|
+
# validation: array
|
51
|
+
param_schema.min_items = param_data["minItems"] unless param_data["minItems"].nil?
|
52
|
+
param_schema.max_items = param_data["maxItems"] unless param_data["maxItems"].nil?
|
53
|
+
param_schema.unique_items = param_data["uniqueItems"] unless param_data["uniqueItems"].nil?
|
54
|
+
|
55
|
+
# validation: number/integer
|
56
|
+
param_schema.min = param_data["minimum"] unless param_data["minimum"].nil?
|
57
|
+
param_schema.min_exclusive = param_data["exclusiveMinimum"] unless param_data["exclusiveMinimum"].nil?
|
58
|
+
param_schema.max = param_data["maximum"] unless param_data["maximum"].nil?
|
59
|
+
param_schema.max_exclusive = param_data["exclusiveMaximum"] unless param_data["exclusiveMaximum"].nil?
|
60
|
+
param_schema.multiple_of = param_data["multipleOf"] unless param_data["multipleOf"].nil?
|
61
|
+
|
62
|
+
# And same idea: despite parameters not being schemas, the items
|
63
|
+
# key (if preset) is actually a schema that defines each item of an
|
64
|
+
# array type, so we can just reflect that directly onto our
|
65
|
+
# artificial schema.
|
66
|
+
if param_data["type"] == "array" && param_data["items"]
|
67
|
+
param_schema.items = param_data["items"]
|
68
|
+
end
|
69
|
+
|
70
|
+
link_schema.properties[param_data["name"]] = param_schema
|
71
|
+
if param_data["required"] == true
|
72
|
+
link_schema.required << param_data["name"]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
[link_schema, nil]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI2
|
6
|
+
class Schema < ::Committee::Drivers::Schema
|
7
|
+
attr_accessor :base_path
|
8
|
+
attr_accessor :consumes
|
9
|
+
|
10
|
+
# A link back to the derivative instance of Committee::Drivers::Driver
|
11
|
+
# that create this schema.
|
12
|
+
attr_accessor :driver
|
13
|
+
|
14
|
+
attr_accessor :definitions
|
15
|
+
attr_accessor :produces
|
16
|
+
attr_accessor :routes
|
17
|
+
attr_reader :validator_option
|
18
|
+
|
19
|
+
def build_router(options)
|
20
|
+
@validator_option = Committee::SchemaValidator::Option.new(options, self, :hyper_schema)
|
21
|
+
Committee::SchemaValidator::HyperSchema::Router.new(self, @validator_option)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI2
|
6
|
+
class SchemaBuilder
|
7
|
+
def initialize(link_data)
|
8
|
+
@link_data = link_data
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
LINK_REQUIRED_FIELDS = [
|
14
|
+
:name
|
15
|
+
].map(&:to_s).freeze
|
16
|
+
|
17
|
+
attr_accessor :link_data
|
18
|
+
|
19
|
+
def check_required_fields!(param_data)
|
20
|
+
LINK_REQUIRED_FIELDS.each do |field|
|
21
|
+
if !param_data[field]
|
22
|
+
raise ArgumentError,
|
23
|
+
"Committee: no #{field} section in link data."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
require_relative 'header_schema_builder'
|
33
|
+
require_relative 'parameter_schema_builder'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI2
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require_relative 'open_api_2/driver'
|
11
|
+
require_relative 'open_api_2/link'
|
12
|
+
require_relative 'open_api_2/schema'
|
13
|
+
require_relative 'open_api_2/schema_builder'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI3
|
6
|
+
class Driver < ::Committee::Drivers::Driver
|
7
|
+
def default_coerce_date_times
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
# Whether parameters that were form-encoded will be coerced by default.
|
12
|
+
def default_coerce_form_params
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_allow_get_body
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Whether parameters in a request's path will be considered and coerced by
|
21
|
+
# default.
|
22
|
+
def default_path_params
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
# Whether parameters in a request's query string will be considered and
|
27
|
+
# coerced by default.
|
28
|
+
def default_query_params
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_validate_success_only
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def name
|
37
|
+
:open_api_3
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Committee::Drivers::OpenAPI3::Schema]
|
41
|
+
def parse(open_api)
|
42
|
+
schema_class.new(self, open_api)
|
43
|
+
end
|
44
|
+
|
45
|
+
def schema_class
|
46
|
+
Committee::Drivers::OpenAPI3::Schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
module OpenAPI3
|
6
|
+
class Schema < ::Committee::Drivers::Schema
|
7
|
+
attr_reader :open_api
|
8
|
+
attr_reader :validator_option
|
9
|
+
|
10
|
+
# @!attribute [r] open_api
|
11
|
+
# @return [OpenAPIParser::Schemas::OpenAPI]
|
12
|
+
|
13
|
+
def initialize(driver, open_api)
|
14
|
+
@open_api = open_api
|
15
|
+
@driver = driver
|
16
|
+
end
|
17
|
+
|
18
|
+
def supports_stub?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def driver # we don't use attr_reader because this method override super class
|
23
|
+
@driver
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_router(options)
|
27
|
+
@validator_option = Committee::SchemaValidator::Option.new(options, self, :open_api_3)
|
28
|
+
Committee::SchemaValidator::OpenAPI3::Router.new(self, @validator_option)
|
29
|
+
end
|
30
|
+
|
31
|
+
# OpenAPI3 only
|
32
|
+
def operation_object(path, method)
|
33
|
+
request_operation = open_api.request_operation(method, path)
|
34
|
+
return nil unless request_operation
|
35
|
+
|
36
|
+
Committee::SchemaValidator::OpenAPI3::OperationWrapper.new(request_operation)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
# Schema is a base class for driver schema implementations.
|
6
|
+
class Schema
|
7
|
+
# A link back to the derivative instance of Committee::Drivers::Driver
|
8
|
+
# that create this schema.
|
9
|
+
def driver
|
10
|
+
raise "needs implementation"
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_router(options)
|
14
|
+
raise "needs implementation"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Stubs are supported in JSON Hyper-Schema and OpenAPI 2, but not yet in OpenAPI 3
|
18
|
+
def supports_stub?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Drivers
|
5
|
+
# Gets a driver instance from the specified name. Raises ArgumentError for
|
6
|
+
# an unknown driver name.
|
7
|
+
def self.driver_from_name(name)
|
8
|
+
case name
|
9
|
+
when :hyper_schema
|
10
|
+
Committee::Drivers::HyperSchema::Driver.new
|
11
|
+
when :open_api_2
|
12
|
+
Committee::Drivers::OpenAPI2::Driver.new
|
13
|
+
when :open_api_3
|
14
|
+
Committee::Drivers::OpenAPI3::Driver.new
|
15
|
+
else
|
16
|
+
raise ArgumentError, %{Committee: unknown driver "#{name}".}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# load and build drive from JSON file
|
21
|
+
# @param [String] schema_path
|
22
|
+
# @return [Committee::Driver]
|
23
|
+
def self.load_from_json(schema_path, parser_options: {})
|
24
|
+
load_from_data(JSON.parse(File.read(schema_path)), schema_path, parser_options: parser_options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# load and build drive from YAML file
|
28
|
+
# @param [String] schema_path
|
29
|
+
# @return [Committee::Driver]
|
30
|
+
def self.load_from_yaml(schema_path, parser_options: {})
|
31
|
+
data = YAML.respond_to?(:unsafe_load_file) ? YAML.unsafe_load_file(schema_path) : YAML.load_file(schema_path)
|
32
|
+
load_from_data(data, schema_path, parser_options: parser_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# load and build drive from file
|
36
|
+
# @param [String] schema_path
|
37
|
+
# @return [Committee::Driver]
|
38
|
+
def self.load_from_file(schema_path, parser_options: {})
|
39
|
+
case File.extname(schema_path)
|
40
|
+
when '.json'
|
41
|
+
load_from_json(schema_path, parser_options: parser_options)
|
42
|
+
when '.yaml', '.yml'
|
43
|
+
load_from_yaml(schema_path, parser_options: parser_options)
|
44
|
+
else
|
45
|
+
raise "Committee only supports the following file extensions: '.json', '.yaml', '.yml'"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# load and build drive from Hash object
|
50
|
+
# @param [Hash] hash
|
51
|
+
# @return [Committee::Driver]
|
52
|
+
def self.load_from_data(hash, schema_path = nil, parser_options: {})
|
53
|
+
if hash['openapi']&.start_with?('3.0.')
|
54
|
+
# From the next major version, we want to ensure `{ strict_reference_validation: true }`
|
55
|
+
# as a parser option here, but since it may break existing implementations, just warn
|
56
|
+
# if it is not explicitly set. See: https://github.com/interagent/committee/issues/343#issuecomment-997400329
|
57
|
+
opts = parser_options.dup
|
58
|
+
|
59
|
+
Committee.warn_deprecated_until_6(!opts.key?(:strict_reference_validation), 'openapi_parser will default to strict reference validation ' +
|
60
|
+
'from next version. Pass config `strict_reference_validation: true` (or false, if you must) ' +
|
61
|
+
'to quiet this warning.')
|
62
|
+
opts[:strict_reference_validation] ||= false
|
63
|
+
|
64
|
+
openapi = OpenAPIParser.parse_with_filepath(hash, schema_path, opts)
|
65
|
+
return Committee::Drivers::OpenAPI3::Driver.new.parse(openapi)
|
66
|
+
end
|
67
|
+
|
68
|
+
driver = if hash['swagger'] == '2.0'
|
69
|
+
Committee::Drivers::OpenAPI2::Driver.new
|
70
|
+
else
|
71
|
+
Committee::Drivers::HyperSchema::Driver.new
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: in the future, pass `opts` here and allow optionality in other drivers?
|
75
|
+
driver.parse(hash)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
require_relative "drivers/driver"
|
81
|
+
require_relative "drivers/schema"
|
82
|
+
require_relative "drivers/hyper_schema"
|
83
|
+
require_relative "drivers/open_api_2"
|
84
|
+
require_relative "drivers/open_api_3"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class BadRequest < Error
|
8
|
+
end
|
9
|
+
|
10
|
+
class InvalidRequest < Error
|
11
|
+
attr_reader :original_error
|
12
|
+
|
13
|
+
def initialize(error_message=nil, original_error: nil)
|
14
|
+
@original_error = original_error
|
15
|
+
super(error_message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class InvalidResponse < Error
|
20
|
+
attr_reader :original_error
|
21
|
+
|
22
|
+
def initialize(error_message=nil, original_error: nil)
|
23
|
+
@original_error = original_error
|
24
|
+
super(error_message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NotFound < Error
|
29
|
+
end
|
30
|
+
|
31
|
+
class ReferenceNotFound < Error
|
32
|
+
end
|
33
|
+
|
34
|
+
class OpenAPI3Unsupported < Error
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Middleware
|
5
|
+
class Base
|
6
|
+
def initialize(app, options={})
|
7
|
+
@app = app
|
8
|
+
|
9
|
+
@error_class = options.fetch(:error_class, Committee::ValidationError)
|
10
|
+
@error_handler = options[:error_handler]
|
11
|
+
@ignore_error = options.fetch(:ignore_error, false)
|
12
|
+
|
13
|
+
@raise = options[:raise]
|
14
|
+
@schema = self.class.get_schema(options)
|
15
|
+
|
16
|
+
@router = @schema.build_router(options)
|
17
|
+
@accept_request_filter = options[:accept_request_filter] || -> (_) { true }
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
request = Rack::Request.new(env)
|
22
|
+
|
23
|
+
if @router.includes_request?(request) && @accept_request_filter.call(request)
|
24
|
+
handle(request)
|
25
|
+
else
|
26
|
+
@app.call(request.env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def get_schema(options)
|
32
|
+
schema = options[:schema]
|
33
|
+
if !schema && options[:schema_path]
|
34
|
+
# In the future, we could have `parser_options` as an exposed config?
|
35
|
+
parser_options = options.key?(:strict_reference_validation) ? { strict_reference_validation: options[:strict_reference_validation] } : {}
|
36
|
+
schema = Committee::Drivers::load_from_file(options[:schema_path], parser_options: parser_options)
|
37
|
+
end
|
38
|
+
raise(ArgumentError, "Committee: need option `schema` or `schema_path`") unless schema
|
39
|
+
|
40
|
+
# Expect the type we want by now. If we don't have it, the user passed
|
41
|
+
# something else non-standard in.
|
42
|
+
if !schema.is_a?(Committee::Drivers::Schema)
|
43
|
+
raise ArgumentError, "Committee: schema expected to be an instance of Committee::Drivers::Schema."
|
44
|
+
end
|
45
|
+
|
46
|
+
return schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def build_schema_validator(request)
|
53
|
+
@router.build_schema_validator(request)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Middleware
|
5
|
+
class RequestValidation < Base
|
6
|
+
def initialize(app, options={})
|
7
|
+
super
|
8
|
+
|
9
|
+
@strict = options[:strict]
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle(request)
|
13
|
+
begin
|
14
|
+
schema_validator = build_schema_validator(request)
|
15
|
+
schema_validator.request_validate(request)
|
16
|
+
|
17
|
+
raise Committee::NotFound, "That request method and path combination isn't defined." if !schema_validator.link_exist? && @strict
|
18
|
+
rescue Committee::BadRequest, Committee::InvalidRequest
|
19
|
+
handle_exception($!, request.env)
|
20
|
+
raise if @raise
|
21
|
+
return @error_class.new(400, :bad_request, $!.message, request).render unless @ignore_error
|
22
|
+
rescue Committee::NotFound => e
|
23
|
+
raise if @raise
|
24
|
+
return @error_class.new(404, :not_found, e.message, request).render unless @ignore_error
|
25
|
+
rescue JSON::ParserError
|
26
|
+
handle_exception($!, request.env)
|
27
|
+
raise Committee::InvalidRequest if @raise
|
28
|
+
return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.", request).render unless @ignore_error
|
29
|
+
end
|
30
|
+
|
31
|
+
@app.call(request.env)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def handle_exception(e, env)
|
37
|
+
@error_handler.call(e, env) if @error_handler
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Middleware
|
5
|
+
class ResponseValidation < Base
|
6
|
+
attr_reader :validate_success_only
|
7
|
+
|
8
|
+
def initialize(app, options = {})
|
9
|
+
super
|
10
|
+
@strict = options[:strict]
|
11
|
+
@validate_success_only = @schema.validator_option.validate_success_only
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle(request)
|
15
|
+
status, headers, response = @app.call(request.env)
|
16
|
+
|
17
|
+
begin
|
18
|
+
v = build_schema_validator(request)
|
19
|
+
v.response_validate(status, headers, response, @strict) if v.link_exist? && self.class.validate?(status, validate_success_only)
|
20
|
+
|
21
|
+
rescue Committee::InvalidResponse
|
22
|
+
handle_exception($!, request.env)
|
23
|
+
|
24
|
+
raise if @raise
|
25
|
+
return @error_class.new(500, :invalid_response, $!.message).render unless @ignore_error
|
26
|
+
rescue JSON::ParserError
|
27
|
+
handle_exception($!, request.env)
|
28
|
+
|
29
|
+
raise Committee::InvalidResponse if @raise
|
30
|
+
return @error_class.new(500, :invalid_response, "Response wasn't valid JSON.").render unless @ignore_error
|
31
|
+
end
|
32
|
+
|
33
|
+
[status, headers, response]
|
34
|
+
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def validate?(status, validate_success_only)
|
38
|
+
case status
|
39
|
+
when 204
|
40
|
+
false
|
41
|
+
when 200..299
|
42
|
+
true
|
43
|
+
when 304
|
44
|
+
false
|
45
|
+
else
|
46
|
+
!validate_success_only
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def handle_exception(e, env)
|
54
|
+
@error_handler.call(e, env) if @error_handler
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Stub is not yet supported in OpenAPI 3
|
4
|
+
|
5
|
+
module Committee
|
6
|
+
module Middleware
|
7
|
+
class Stub < Base
|
8
|
+
def initialize(app, options={})
|
9
|
+
super
|
10
|
+
|
11
|
+
# A bug in Committee's cache implementation meant that it wasn't working
|
12
|
+
# for a very long time, even for people who thought they were taking
|
13
|
+
# advantage of it. I repaired the caching feature, but have disable it by
|
14
|
+
# default so that we don't need to introduce any class-level variables
|
15
|
+
# that could have memory leaking implications. To enable caching, just
|
16
|
+
# pass an empty hash to this option.
|
17
|
+
@cache = options[:cache]
|
18
|
+
|
19
|
+
@call = options[:call]
|
20
|
+
|
21
|
+
raise Committee::OpenAPI3Unsupported.new("Stubs are not yet supported for OpenAPI 3") unless @schema.supports_stub?
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle(request)
|
25
|
+
link, _ = @router.find_request_link(request)
|
26
|
+
if link
|
27
|
+
headers = { "Content-Type" => "application/json" }
|
28
|
+
|
29
|
+
data, schema = cache(link) do
|
30
|
+
Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
|
31
|
+
end
|
32
|
+
|
33
|
+
if @call
|
34
|
+
request.env["committee.response"] = data
|
35
|
+
request.env["committee.response_schema"] = schema
|
36
|
+
call_status, call_headers, call_body = @app.call(request.env)
|
37
|
+
|
38
|
+
# a committee.suppress signal initiates a direct pass through
|
39
|
+
if request.env["committee.suppress"] == true
|
40
|
+
return call_status, call_headers, call_body
|
41
|
+
end
|
42
|
+
|
43
|
+
# otherwise keep the headers and whatever data manipulations were
|
44
|
+
# made, and stub normally
|
45
|
+
headers.merge!(call_headers)
|
46
|
+
|
47
|
+
# allow the handler to change the data object (if unchanged, it
|
48
|
+
# will be the same one that we set above)
|
49
|
+
data = request.env["committee.response"]
|
50
|
+
end
|
51
|
+
|
52
|
+
[link.status_success, headers, [JSON.pretty_generate(data)]]
|
53
|
+
else
|
54
|
+
@app.call(request.env)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def cache(link)
|
61
|
+
return yield unless @cache
|
62
|
+
|
63
|
+
# Just the object ID is enough to uniquely identify the link, but store
|
64
|
+
# the method and href so that we can more easily introspect the cache if
|
65
|
+
# necessary.
|
66
|
+
key = "#{link.object_id}##{link.method}+#{link.href}"
|
67
|
+
if @cache[key]
|
68
|
+
@cache[key]
|
69
|
+
else
|
70
|
+
@cache[key] = yield
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committee
|
4
|
+
module Middleware
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative "middleware/base"
|
9
|
+
require_relative "middleware/request_validation"
|
10
|
+
require_relative "middleware/response_validation"
|
11
|
+
require_relative "middleware/stub"
|