Skip to content

Commit 8665b13

Browse files
authored
Add more straightforward options by type (#463)
Fixes: #347
1 parent 568c77e commit 8665b13

File tree

12 files changed

+689
-9
lines changed

12 files changed

+689
-9
lines changed

lib/committee.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def self.warn_deprecated_until_6(cond, message)
3030
require_relative "committee/utils"
3131
require_relative "committee/drivers"
3232
require_relative "committee/errors"
33+
require_relative "committee/validation_error"
3334
require_relative "committee/middleware"
3435
require_relative "committee/request_unpacker"
3536
require_relative "committee/schema_validator"
36-
require_relative "committee/validation_error"
3737

3838
require_relative "committee/bin/committee_stub"
3939
require_relative "committee/test/methods"

lib/committee/middleware.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module Middleware
55
end
66
end
77

8+
require_relative "middleware/options"
89
require_relative "middleware/base"
910
require_relative "middleware/request_validation"
1011
require_relative "middleware/response_validation"

lib/committee/middleware/base.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ module Middleware
55
class Base
66
def initialize(app, options = {})
77
@app = app
8+
@options = build_options(options)
89

9-
@error_class = options.fetch(:error_class, Committee::ValidationError)
10-
@error_handler = options[:error_handler]
11-
@ignore_error = options.fetch(:ignore_error, false)
10+
@error_class = @options.error_class
11+
@error_handler = @options.error_handler
12+
@ignore_error = @options.ignore_error
1213

13-
@raise = options[:raise]
14+
@raise = @options.raise_error
1415
@schema = self.class.get_schema(options)
1516

1617
@router = @schema.build_router(options)
17-
@accept_request_filter = options[:accept_request_filter] || ->(_) { true }
18+
@accept_request_filter = @options.accept_request_filter
1819
end
1920

2021
def call(env)
@@ -49,6 +50,11 @@ def get_schema(options)
4950

5051
private
5152

53+
# Subclasses should override this method to use their specific Options class
54+
def build_options(options)
55+
Options::Base.from(options)
56+
end
57+
5258
def build_schema_validator(request)
5359
@router.build_schema_validator(request)
5460
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module Committee
4+
module Middleware
5+
module Options
6+
end
7+
end
8+
end
9+
10+
require_relative "options/base"
11+
require_relative "options/request_validation"
12+
require_relative "options/response_validation"
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# frozen_string_literal: true
2+
3+
module Committee
4+
module Middleware
5+
module Options
6+
class Base
7+
# Common options
8+
attr_reader :schema
9+
attr_reader :schema_path
10+
attr_reader :error_class
11+
attr_reader :error_handler
12+
attr_reader :raise_error
13+
attr_reader :ignore_error
14+
attr_reader :prefix
15+
attr_reader :accept_request_filter
16+
17+
# Schema loading options
18+
attr_reader :strict_reference_validation
19+
20+
def initialize(options = {})
21+
@original_options = options.dup.freeze
22+
23+
# Schema related
24+
@schema = options[:schema]
25+
@schema_path = options[:schema_path]
26+
@strict_reference_validation = options.fetch(:strict_reference_validation, false)
27+
28+
# Error handling
29+
@error_class = options.fetch(:error_class, Committee::ValidationError)
30+
@error_handler = options[:error_handler]
31+
@raise_error = options[:raise] || false
32+
@ignore_error = options.fetch(:ignore_error, false)
33+
34+
# Routing
35+
@prefix = options[:prefix]
36+
@accept_request_filter = options[:accept_request_filter] || ->(_) { true }
37+
38+
validate!
39+
end
40+
41+
# Allow Hash-like access for backward compatibility
42+
def [](key)
43+
to_h[key]
44+
end
45+
46+
def fetch(key, default = nil)
47+
to_h.fetch(key, default)
48+
end
49+
50+
def to_h
51+
@to_h ||= build_hash
52+
end
53+
54+
# Create an Option object from Hash options
55+
# Returns the object as-is if already an Option object
56+
def self.from(options)
57+
case options
58+
when self
59+
options
60+
when Hash
61+
new(options)
62+
else
63+
raise ArgumentError, "options must be a Hash or #{name}"
64+
end
65+
end
66+
67+
private
68+
69+
def validate!
70+
validate_error_class!
71+
validate_error_handler!
72+
validate_schema_source!
73+
validate_accept_request_filter!
74+
end
75+
76+
def validate_error_handler!
77+
return if @error_handler.nil? || @error_handler.respond_to?(:call)
78+
79+
raise ArgumentError, "error_handler must respond to #call"
80+
end
81+
82+
def validate_error_class!
83+
return if @error_class.is_a?(Class)
84+
85+
raise ArgumentError, "error_class must be a Class"
86+
end
87+
88+
def validate_schema_source!
89+
return if @schema || @schema_path
90+
91+
raise ArgumentError, "Committee: need option `schema` or `schema_path`"
92+
end
93+
94+
def validate_accept_request_filter!
95+
return if @accept_request_filter.respond_to?(:call)
96+
97+
raise ArgumentError, "accept_request_filter must respond to #call"
98+
end
99+
100+
def build_hash
101+
# Start with original options to preserve any options not explicitly handled
102+
@original_options.dup.merge(schema: @schema, schema_path: @schema_path, strict_reference_validation: @strict_reference_validation, error_class: @error_class, error_handler: @error_handler, raise: @raise_error, ignore_error: @ignore_error, prefix: @prefix, accept_request_filter: @accept_request_filter)
103+
end
104+
end
105+
end
106+
end
107+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
module Committee
4+
module Middleware
5+
module Options
6+
class RequestValidation < Base
7+
# RequestValidation specific options
8+
attr_reader :strict
9+
attr_reader :coerce_date_times
10+
attr_reader :coerce_recursive
11+
attr_reader :coerce_form_params
12+
attr_reader :coerce_path_params
13+
attr_reader :coerce_query_params
14+
attr_reader :allow_form_params
15+
attr_reader :allow_query_params
16+
attr_reader :allow_get_body
17+
attr_reader :allow_non_get_query_params
18+
attr_reader :check_content_type
19+
attr_reader :check_header
20+
attr_reader :optimistic_json
21+
attr_reader :request_body_hash_key
22+
attr_reader :query_hash_key
23+
attr_reader :path_hash_key
24+
attr_reader :headers_key
25+
attr_reader :params_key
26+
attr_reader :allow_blank_structures
27+
attr_reader :allow_empty_date_and_datetime
28+
attr_reader :parameter_overwrite_by_rails_rule
29+
attr_reader :deserialize_parameters
30+
31+
# Default values
32+
DEFAULTS = { strict: false, coerce_recursive: true, allow_get_body: false, check_content_type: true, check_header: true, optimistic_json: false, allow_form_params: true, allow_query_params: true, allow_non_get_query_params: false, allow_blank_structures: false, allow_empty_date_and_datetime: false, parameter_overwrite_by_rails_rule: true, request_body_hash_key: "committee.request_body_hash", query_hash_key: "committee.query_hash", path_hash_key: "committee.path_hash", headers_key: "committee.headers", params_key: "committee.params" }.freeze
33+
34+
def initialize(options = {})
35+
super(options)
36+
37+
# Validation related
38+
@strict = options.fetch(:strict, DEFAULTS[:strict])
39+
@check_content_type = options.fetch(:check_content_type, DEFAULTS[:check_content_type])
40+
@check_header = options.fetch(:check_header, DEFAULTS[:check_header])
41+
@optimistic_json = options.fetch(:optimistic_json, DEFAULTS[:optimistic_json])
42+
43+
# Type coercion related
44+
@coerce_date_times = options[:coerce_date_times]
45+
@coerce_recursive = options.fetch(:coerce_recursive, DEFAULTS[:coerce_recursive])
46+
@coerce_form_params = options[:coerce_form_params]
47+
@coerce_path_params = options[:coerce_path_params]
48+
@coerce_query_params = options[:coerce_query_params]
49+
50+
# Parameter permission related
51+
@allow_form_params = options.fetch(:allow_form_params, DEFAULTS[:allow_form_params])
52+
@allow_query_params = options.fetch(:allow_query_params, DEFAULTS[:allow_query_params])
53+
@allow_get_body = options.fetch(:allow_get_body, DEFAULTS[:allow_get_body])
54+
@allow_non_get_query_params = options.fetch(:allow_non_get_query_params, DEFAULTS[:allow_non_get_query_params])
55+
@allow_blank_structures = options.fetch(:allow_blank_structures, DEFAULTS[:allow_blank_structures])
56+
@allow_empty_date_and_datetime = options.fetch(:allow_empty_date_and_datetime, DEFAULTS[:allow_empty_date_and_datetime])
57+
58+
# Rails compatibility
59+
@parameter_overwrite_by_rails_rule = options.fetch(:parameter_overwrite_by_rails_rule, DEFAULTS[:parameter_overwrite_by_rails_rule])
60+
61+
# Deserialization
62+
@deserialize_parameters = options[:deserialize_parameters]
63+
64+
# Hash keys
65+
@request_body_hash_key = options.fetch(:request_body_hash_key, DEFAULTS[:request_body_hash_key])
66+
@query_hash_key = options.fetch(:query_hash_key, DEFAULTS[:query_hash_key])
67+
@path_hash_key = options.fetch(:path_hash_key, DEFAULTS[:path_hash_key])
68+
@headers_key = options.fetch(:headers_key, DEFAULTS[:headers_key])
69+
@params_key = options.fetch(:params_key, DEFAULTS[:params_key])
70+
end
71+
72+
private
73+
74+
def build_hash
75+
super.merge(strict: @strict, coerce_date_times: @coerce_date_times, coerce_recursive: @coerce_recursive, coerce_form_params: @coerce_form_params, coerce_path_params: @coerce_path_params, coerce_query_params: @coerce_query_params, allow_form_params: @allow_form_params, allow_query_params: @allow_query_params, allow_get_body: @allow_get_body, allow_non_get_query_params: @allow_non_get_query_params, allow_blank_structures: @allow_blank_structures, allow_empty_date_and_datetime: @allow_empty_date_and_datetime, parameter_overwrite_by_rails_rule: @parameter_overwrite_by_rails_rule, deserialize_parameters: @deserialize_parameters, check_content_type: @check_content_type, check_header: @check_header, optimistic_json: @optimistic_json, request_body_hash_key: @request_body_hash_key, query_hash_key: @query_hash_key, path_hash_key: @path_hash_key, headers_key: @headers_key, params_key: @params_key)
76+
end
77+
end
78+
end
79+
end
80+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module Committee
4+
module Middleware
5+
module Options
6+
class ResponseValidation < Base
7+
# ResponseValidation specific options
8+
attr_reader :strict
9+
attr_reader :validate_success_only
10+
attr_reader :parse_response_by_content_type
11+
attr_reader :coerce_response_values
12+
attr_reader :streaming_content_parsers
13+
14+
# Default values
15+
DEFAULTS = { strict: false, validate_success_only: true, parse_response_by_content_type: true, coerce_response_values: false }.freeze
16+
17+
def initialize(options = {})
18+
super(options)
19+
20+
# Validation related
21+
@strict = options.fetch(:strict, DEFAULTS[:strict])
22+
@validate_success_only = options.fetch(:validate_success_only, DEFAULTS[:validate_success_only])
23+
@parse_response_by_content_type = options.fetch(:parse_response_by_content_type, DEFAULTS[:parse_response_by_content_type])
24+
@coerce_response_values = options.fetch(:coerce_response_values, DEFAULTS[:coerce_response_values])
25+
26+
# Streaming
27+
@streaming_content_parsers = options[:streaming_content_parsers] || {}
28+
29+
validate_response_validation_options!
30+
end
31+
32+
private
33+
34+
def validate_response_validation_options!
35+
return if @streaming_content_parsers.is_a?(Hash)
36+
37+
raise ArgumentError, "streaming_content_parsers must be a Hash"
38+
end
39+
40+
def build_hash
41+
super.merge(strict: @strict, validate_success_only: @validate_success_only, parse_response_by_content_type: @parse_response_by_content_type, coerce_response_values: @coerce_response_values, streaming_content_parsers: @streaming_content_parsers)
42+
end
43+
end
44+
end
45+
end
46+
end

lib/committee/middleware/request_validation.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ class RequestValidation < Base
66
def initialize(app, options = {})
77
super
88

9-
@strict = options[:strict]
9+
@strict = @options.strict
10+
end
11+
12+
private
13+
14+
def build_options(options)
15+
Options::RequestValidation.from(options)
1016
end
1117

1218
def handle(request)

lib/committee/middleware/response_validation.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class ResponseValidation < Base
77

88
def initialize(app, options = {})
99
super
10-
@strict = options[:strict]
10+
@strict = @options.strict
1111
@validate_success_only = @schema.validator_option.validate_success_only
12-
@streaming_content_parsers = options[:streaming_content_parsers] || {}
12+
@streaming_content_parsers = @options.streaming_content_parsers
1313
end
1414

1515
def handle(request)
@@ -63,6 +63,10 @@ def validate?(status, validate_success_only)
6363

6464
private
6565

66+
def build_options(options)
67+
Options::ResponseValidation.from(options)
68+
end
69+
6670
def validate(request, status, headers, response, streaming_content_parser = nil)
6771
v = build_schema_validator(request)
6872
if v.link_exist? && self.class.validate?(status, validate_success_only)

0 commit comments

Comments
 (0)