committee_firetail 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/bin/committee-stub +23 -0
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +36 -0
  22. data/lib/committee/middleware/base.rb +57 -0
  23. data/lib/committee/middleware/request_validation.rb +41 -0
  24. data/lib/committee/middleware/response_validation.rb +58 -0
  25. data/lib/committee/middleware/stub.rb +75 -0
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +91 -0
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +84 -0
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +26 -0
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +40 -0
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +130 -0
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +516 -0
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +189 -0
  64. data/test/middleware/stub_test.rb +145 -0
  65. data/test/request_unpacker_test.rb +200 -0
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +363 -0
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +120 -0
  79. data/test/validation_error_test.rb +25 -0
  80. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Drivers
5
+ module OpenAPI3
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'open_api_3/driver'
11
+ require_relative 'open_api_3/schema'
@@ -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"