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.
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"