grape 3.1.1 → 3.2.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 +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +76 -161
- data/UPGRADING.md +106 -0
- data/grape.gemspec +2 -2
- data/lib/grape/api/instance.rb +1 -1
- data/lib/grape/api.rb +1 -1
- data/lib/grape/declared_params_handler.rb +3 -3
- data/lib/grape/dsl/declared.rb +1 -1
- data/lib/grape/dsl/desc.rb +1 -1
- data/lib/grape/dsl/inside_route.rb +9 -9
- data/lib/grape/dsl/parameters.rb +14 -14
- data/lib/grape/dsl/routing.rb +8 -8
- data/lib/grape/endpoint.rb +42 -49
- data/lib/grape/error_formatter/base.rb +2 -2
- data/lib/grape/exceptions/base.rb +18 -44
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/validation.rb +7 -4
- data/lib/grape/exceptions/validation_errors.rb +13 -7
- data/lib/grape/locale/en.yml +0 -5
- data/lib/grape/middleware/auth/base.rb +2 -0
- data/lib/grape/middleware/base.rb +2 -4
- data/lib/grape/middleware/error.rb +2 -2
- data/lib/grape/middleware/formatter.rb +1 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/request.rb +2 -10
- data/lib/grape/router/pattern.rb +1 -1
- data/lib/grape/router.rb +4 -2
- data/lib/grape/util/api_description.rb +1 -1
- data/lib/grape/util/deep_freeze.rb +35 -0
- data/lib/grape/util/inheritable_setting.rb +1 -1
- data/lib/grape/util/media_type.rb +1 -1
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +33 -18
- data/lib/grape/validations/contract_scope.rb +1 -7
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/param_scope_tracker.rb +57 -0
- data/lib/grape/validations/params_scope.rb +111 -107
- data/lib/grape/validations/single_attribute_iterator.rb +2 -2
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +95 -18
- data/lib/grape/validations/validators/coerce_validator.rb +15 -35
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -8
- data/lib/grape/validations/validators/default_validator.rb +12 -18
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +13 -4
- data/lib/grape/validations/validators/length_validator.rb +21 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
- data/lib/grape/validations/validators/mutually_exclusive_validator.rb +3 -1
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +8 -10
- data/lib/grape/validations/validators/same_as_validator.rb +6 -15
- data/lib/grape/validations/validators/values_validator.rb +29 -21
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +18 -1
- metadata +11 -13
- data/lib/grape/exceptions/conflicting_types.rb +0 -11
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/invalid_parameters.rb +0 -11
- data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/validations/validator_factory.rb +0 -15
data/lib/grape/request.rb
CHANGED
|
@@ -166,16 +166,8 @@ module Grape
|
|
|
166
166
|
|
|
167
167
|
def make_params
|
|
168
168
|
@params_builder.call(rack_params).deep_merge!(grape_routing_args)
|
|
169
|
-
rescue
|
|
170
|
-
raise Grape::Exceptions::
|
|
171
|
-
rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError
|
|
172
|
-
raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
|
|
173
|
-
rescue Rack::QueryParser::ParamsTooDeepError
|
|
174
|
-
raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
|
|
175
|
-
rescue Rack::Utils::ParameterTypeError
|
|
176
|
-
raise Grape::Exceptions::ConflictingTypes
|
|
177
|
-
rescue Rack::Utils::InvalidParameterError
|
|
178
|
-
raise Grape::Exceptions::InvalidParameters
|
|
169
|
+
rescue *Grape::RACK_ERRORS
|
|
170
|
+
raise Grape::Exceptions::RequestError
|
|
179
171
|
end
|
|
180
172
|
|
|
181
173
|
def build_headers
|
data/lib/grape/router/pattern.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Grape
|
|
|
16
16
|
def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:)
|
|
17
17
|
@origin = origin
|
|
18
18
|
@path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
|
|
19
|
-
@pattern = Mustermann::Grape.new(@path, uri_decode: true, params
|
|
19
|
+
@pattern = Mustermann::Grape.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
|
|
20
20
|
@to_regexp = @pattern.to_regexp
|
|
21
21
|
end
|
|
22
22
|
|
data/lib/grape/router.rb
CHANGED
|
@@ -76,6 +76,9 @@ module Grape
|
|
|
76
76
|
any.endpoint
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
DEFAULT_RESPONSE_HEADERS = Grape::Util::Header.new.merge('X-Cascade' => 'pass').freeze
|
|
80
|
+
DEFAULT_RESPONSE_BODY = ['404 Not Found'].freeze
|
|
81
|
+
|
|
79
82
|
private
|
|
80
83
|
|
|
81
84
|
def identity(input, method, env)
|
|
@@ -146,8 +149,7 @@ module Grape
|
|
|
146
149
|
end
|
|
147
150
|
|
|
148
151
|
def default_response
|
|
149
|
-
|
|
150
|
-
[404, headers, ['404 Not Found']]
|
|
152
|
+
[404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
|
|
151
153
|
end
|
|
152
154
|
|
|
153
155
|
def match?(input, method)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
module DeepFreeze
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Recursively freezes Hash (keys and values), Array (elements), and String
|
|
9
|
+
# objects. All other types are returned as-is.
|
|
10
|
+
#
|
|
11
|
+
# Intentionally left unfrozen:
|
|
12
|
+
# - Procs / lambdas — may be deferred DB-backed callables
|
|
13
|
+
# - Coercers (e.g. ArrayCoercer) — use lazy ivar memoization at request time
|
|
14
|
+
# - Classes / Modules — shared constants that must remain open
|
|
15
|
+
# - ParamsScope — self-freezes at the end of its own initialize
|
|
16
|
+
def deep_freeze(obj)
|
|
17
|
+
case obj
|
|
18
|
+
when Hash
|
|
19
|
+
obj.each do |k, v|
|
|
20
|
+
deep_freeze(k)
|
|
21
|
+
deep_freeze(v)
|
|
22
|
+
end
|
|
23
|
+
obj.freeze
|
|
24
|
+
when Array
|
|
25
|
+
obj.each { |v| deep_freeze(v) }
|
|
26
|
+
obj.freeze
|
|
27
|
+
when String
|
|
28
|
+
obj.freeze
|
|
29
|
+
else
|
|
30
|
+
obj
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -55,7 +55,7 @@ module Grape
|
|
|
55
55
|
namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable
|
|
56
56
|
self.route = parent.route.merge(route)
|
|
57
57
|
|
|
58
|
-
point_in_time_copies.
|
|
58
|
+
point_in_time_copies.each { |cloned_one| cloned_one.inherit_from parent }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Create a point-in-time copy of this settings instance, with clones of
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
module Translation
|
|
6
|
+
FALLBACK_LOCALE = :en
|
|
7
|
+
private_constant :FALLBACK_LOCALE
|
|
8
|
+
# Sentinel returned by I18n when a key is missing (passed as the default:
|
|
9
|
+
# value). Using a named class rather than plain Object.new makes it
|
|
10
|
+
# identifiable in debug output and immune to backends that call .to_s on
|
|
11
|
+
# the default before returning it.
|
|
12
|
+
MISSING = Class.new { def inspect = 'Grape::Util::Translation::MISSING' }.new.freeze
|
|
13
|
+
private_constant :MISSING
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Extra keyword args (**) are forwarded verbatim to I18n as interpolation
|
|
18
|
+
# variables (e.g. +min:+, +max:+ from LengthValidator's Hash message).
|
|
19
|
+
# Callers must not pass unintended keyword arguments — any extra keyword
|
|
20
|
+
# will silently become an I18n interpolation variable.
|
|
21
|
+
def translate(key, default: MISSING, scope: 'grape.errors.messages', locale: nil, **)
|
|
22
|
+
i18n_opts = { default:, scope:, ** }
|
|
23
|
+
i18n_opts[:locale] = locale if locale
|
|
24
|
+
message = ::I18n.translate(key, **i18n_opts)
|
|
25
|
+
return message unless message.equal?(MISSING)
|
|
26
|
+
|
|
27
|
+
effective_default = default.equal?(MISSING) ? [*Array(scope), key].join('.') : default
|
|
28
|
+
return effective_default if fallback_locale?(locale) || fallback_locale_unavailable?
|
|
29
|
+
|
|
30
|
+
::I18n.translate(key, default: effective_default, scope:, locale: FALLBACK_LOCALE, **)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fallback_locale?(locale)
|
|
34
|
+
(locale || ::I18n.locale) == FALLBACK_LOCALE
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fallback_locale_unavailable?
|
|
38
|
+
::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -7,9 +7,9 @@ module Grape
|
|
|
7
7
|
|
|
8
8
|
attr_reader :scope
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
10
|
+
def initialize(attrs, scope, params)
|
|
11
|
+
@attrs = attrs
|
|
11
12
|
@scope = scope
|
|
12
|
-
@attrs = validator.attrs
|
|
13
13
|
@original_params = scope.params(params)
|
|
14
14
|
@params = Array.wrap(@original_params)
|
|
15
15
|
end
|
|
@@ -20,39 +20,54 @@ module Grape
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def do_each(params_to_process,
|
|
24
|
-
@scope.reset_index # gets updated depending on the size of params_to_process
|
|
23
|
+
def do_each(params_to_process, parent_indices = [], &block)
|
|
25
24
|
params_to_process.each_with_index do |resource_params, index|
|
|
26
25
|
# when we get arrays of arrays it means that target element located inside array
|
|
27
|
-
# we need this because we want to know parent arrays
|
|
26
|
+
# we need this because we want to know parent arrays indices
|
|
28
27
|
if resource_params.is_a?(Array)
|
|
29
|
-
do_each(resource_params, [index] +
|
|
28
|
+
do_each(resource_params, [index] + parent_indices, &block)
|
|
30
29
|
next
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
if @scope.type == Array
|
|
34
33
|
next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
store_indices(@scope, index, parent_indices)
|
|
36
|
+
elsif @original_params.is_a?(Array)
|
|
37
|
+
# Lateral scope (no @element) whose params resolved to an array —
|
|
38
|
+
# delegate index tracking to the nearest array-typed ancestor so
|
|
39
|
+
# that full_name produces the correct bracketed index.
|
|
40
|
+
target = @scope.nearest_array_ancestor
|
|
41
|
+
store_indices(target, index, parent_indices) if target
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
yield_attributes(resource_params,
|
|
44
|
+
yield_attributes(resource_params, &block)
|
|
46
45
|
end
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
def
|
|
48
|
+
def store_indices(target_scope, index, parent_indices)
|
|
49
|
+
# No tracker means we're outside a ParamScopeTracker.track block (e.g.
|
|
50
|
+
# a unit test that invokes a validator directly). Index tracking is
|
|
51
|
+
# skipped — full_name will produce bracket-less names — but validation
|
|
52
|
+
# continues rather than crashing.
|
|
53
|
+
tracker = ParamScopeTracker.current or return
|
|
54
|
+
parent_scope = target_scope.parent
|
|
55
|
+
parent_indices.each do |parent_index|
|
|
56
|
+
break unless parent_scope
|
|
57
|
+
|
|
58
|
+
tracker.store_index(parent_scope, parent_index)
|
|
59
|
+
parent_scope = parent_scope.parent
|
|
60
|
+
end
|
|
61
|
+
tracker.store_index(target_scope, index)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def yield_attributes(_resource_params)
|
|
50
65
|
raise NotImplementedError
|
|
51
66
|
end
|
|
52
67
|
|
|
53
|
-
# This is a special case so that we can ignore
|
|
54
|
-
# values are missing lower down. Unfortunately we can remove this
|
|
55
|
-
#
|
|
68
|
+
# This is a special case so that we can ignore trees where option
|
|
69
|
+
# values are missing lower down. Unfortunately we can't remove this
|
|
70
|
+
# at the parameter parsing stage as they are required to ensure
|
|
56
71
|
# the correct indexing is maintained
|
|
57
72
|
def skip?(val)
|
|
58
73
|
val == Grape::DSL::Parameters::EmptyOptionalValue
|
|
@@ -21,13 +21,7 @@ module Grape
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map
|
|
24
|
-
|
|
25
|
-
validator_options = {
|
|
26
|
-
validator_class: Grape::Validations.require_validator(:contract_scope),
|
|
27
|
-
opts: { schema: contract, fail_fast: false }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
api.inheritable_setting.namespace_stackable[:validations] = validator_options
|
|
24
|
+
api.inheritable_setting.namespace_stackable[:validations] = Validators::ContractScopeValidator.new(schema: contract)
|
|
31
25
|
end
|
|
32
26
|
end
|
|
33
27
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
# Holds per-request mutable state that must not live on shared ParamsScope
|
|
6
|
+
# instances. Both trackers are identity-keyed hashes so that ParamsScope
|
|
7
|
+
# objects can serve as keys without relying on value equality.
|
|
8
|
+
#
|
|
9
|
+
# Lifecycle is managed by Endpoint#run_validators via +.track+.
|
|
10
|
+
# Use +.current+ to access the instance for the running request.
|
|
11
|
+
class ParamScopeTracker
|
|
12
|
+
# Fiber-local key used to store the current tracker.
|
|
13
|
+
# Fiber[] (Ruby 3.0+) is used instead of Thread.current[] so that
|
|
14
|
+
# fiber-based servers (e.g. Falcon with async) isolate each request's
|
|
15
|
+
# tracker within its own fiber rather than sharing state across all
|
|
16
|
+
# fibers running on the same thread.
|
|
17
|
+
FIBER_KEY = :grape_param_scope_tracker
|
|
18
|
+
EMPTY_PARAMS = [].freeze
|
|
19
|
+
|
|
20
|
+
def self.track
|
|
21
|
+
previous = Fiber[FIBER_KEY]
|
|
22
|
+
Fiber[FIBER_KEY] = new
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
Fiber[FIBER_KEY] = previous
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.current
|
|
29
|
+
Fiber[FIBER_KEY]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@index_tracker = {}.compare_by_identity
|
|
34
|
+
@qualifying_params_tracker = {}.compare_by_identity
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def store_index(scope, index)
|
|
38
|
+
@index_tracker.store(scope, index)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def index_for(scope)
|
|
42
|
+
@index_tracker[scope]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns qualifying params for +scope+, or EMPTY_PARAMS if none were stored.
|
|
46
|
+
# Note: an explicitly stored empty array and "never stored" are treated identically
|
|
47
|
+
# by callers (both yield a blank result that falls through to the parent params).
|
|
48
|
+
def qualifying_params(scope)
|
|
49
|
+
@qualifying_params_tracker.fetch(scope, EMPTY_PARAMS)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def store_qualifying_params(scope, params)
|
|
53
|
+
@qualifying_params_tracker.store(scope, params)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|