grape 3.3.4 → 4.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 +4 -4
- data/CHANGELOG.md +128 -0
- data/CONTRIBUTING.md +16 -0
- data/README.md +208 -8
- data/UPGRADING.md +756 -6
- data/lib/grape/api/instance.rb +40 -33
- data/lib/grape/content_types.rb +47 -1
- data/lib/grape/dry_types.rb +1 -1
- data/lib/grape/dsl/callbacks.rb +3 -9
- data/lib/grape/dsl/declared.rb +4 -4
- data/lib/grape/dsl/desc.rb +18 -8
- data/lib/grape/dsl/entity.rb +37 -19
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +20 -8
- data/lib/grape/dsl/middleware.rb +3 -3
- data/lib/grape/dsl/parameters.rb +54 -28
- data/lib/grape/dsl/request_response.rb +37 -39
- data/lib/grape/dsl/rescue_options.rb +6 -5
- data/lib/grape/dsl/routing.rb +116 -49
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/validations.rb +3 -3
- data/lib/grape/dsl/version_options.rb +3 -3
- data/lib/grape/endpoint/options.rb +13 -7
- data/lib/grape/endpoint.rb +141 -118
- data/lib/grape/env.rb +7 -0
- data/lib/grape/error_formatter/base.rb +6 -5
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/error_response.rb +4 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/validation.rb +9 -8
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +1 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/dsl.rb +5 -33
- data/lib/grape/middleware/error.rb +153 -46
- data/lib/grape/middleware/formatter.rb +95 -26
- data/lib/grape/middleware/precomputed_content_types.rb +12 -7
- data/lib/grape/middleware/stack.rb +13 -4
- data/lib/grape/middleware/versioner/base.rb +3 -9
- data/lib/grape/middleware/versioner/path.rb +49 -7
- data/lib/grape/namespace.rb +3 -2
- data/lib/grape/path.rb +8 -69
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +36 -4
- data/lib/grape/router/base_route.rb +47 -10
- data/lib/grape/router/greedy_route.rb +5 -1
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +60 -14
- data/lib/grape/router/route.rb +62 -10
- data/lib/grape/router.rb +100 -57
- data/lib/grape/serve_stream/file_body.rb +6 -0
- data/lib/grape/serve_stream/stream_response.rb +6 -0
- data/lib/grape/util/api_description.rb +10 -1
- data/lib/grape/util/cache.rb +21 -2
- data/lib/grape/util/deep_freeze.rb +1 -2
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +751 -37
- data/lib/grape/util/media_type.rb +10 -3
- data/lib/grape/util/path_normalizer.rb +12 -9
- data/lib/grape/util/registry.rb +12 -2
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +32 -6
- data/lib/grape/validations/attributes_iterator.rb +21 -4
- data/lib/grape/validations/contract_scope.rb +8 -7
- data/lib/grape/validations/oneof_collector.rb +6 -10
- data/lib/grape/validations/params_documentation.rb +2 -2
- data/lib/grape/validations/params_scope.rb +66 -26
- data/lib/grape/validations/single_attribute_iterator.rb +5 -3
- data/lib/grape/validations/types/array_coercer.rb +4 -6
- data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +12 -9
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
- data/lib/grape/validations/types.rb +5 -5
- data/lib/grape/validations/validations_spec.rb +14 -4
- data/lib/grape/validations/validators/base.rb +4 -7
- data/lib/grape/validations/validators/coerce_validator.rb +8 -2
- data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
- data/lib/grape/validations/validators/length_validator.rb +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +2 -0
- data/lib/grape/validations/validators/same_as_validator.rb +1 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +23 -2
- metadata +10 -9
- data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -33
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
|
@@ -2,19 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# The per-scope settings registry behind the Grape DSL. The semantic
|
|
6
|
+
# accessors below — grouped by concern — are the supported API: +add_*+
|
|
7
|
+
# writers stack one registration per call (read back outermost scope
|
|
8
|
+
# first), plain +=+ writers are nearest-wins scalar overrides, and
|
|
9
|
+
# +!+/+?+ pairs are scope flags. Deep-merged readers return nil when
|
|
10
|
+
# nothing is registered; plain stack readers return a frozen empty
|
|
11
|
+
# Array. The backing stores — a per-scope Hash per kind of state, each
|
|
12
|
+
# holding only what that scope itself set — and their keys are internal.
|
|
13
|
+
#
|
|
14
|
+
# Settings instances form a chain: a scope inherits its parent's values
|
|
15
|
+
# (see #inherit_from), and endpoints snapshot the chain with
|
|
16
|
+
# #point_in_time_copy_for_endpoint. Nothing is copied down the chain when
|
|
17
|
+
# a scope is created; every reader resolves against #parent on demand, so
|
|
18
|
+
# a value an enclosing scope gains later is visible through scopes already
|
|
19
|
+
# nested inside it.
|
|
7
20
|
class InheritableSetting
|
|
8
|
-
|
|
21
|
+
# Maps the callbacks DSL method names to their pluralized
|
|
22
|
+
# namespace-stackable storage keys (see #callbacks / #add_callback).
|
|
23
|
+
CALLBACK_STORE_KEYS = {
|
|
24
|
+
before: :befores,
|
|
25
|
+
before_validation: :before_validations,
|
|
26
|
+
after_validation: :after_validations,
|
|
27
|
+
after: :afters,
|
|
28
|
+
finally: :finallies
|
|
29
|
+
}.freeze
|
|
9
30
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
# Shared empty result for #stacked / #stacked_keys when nothing is
|
|
32
|
+
# registered anywhere in the chain, so neither hands out a mutable Array.
|
|
33
|
+
EMPTY_STACK = [].freeze
|
|
34
|
+
|
|
35
|
+
attr_reader :route, :namespace, :parent
|
|
15
36
|
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
# A StackableValues view of this scope's registrations, rebuilt on each
|
|
38
|
+
# call. Public for ecosystem compatibility only — grape-swagger reads it
|
|
39
|
+
# directly and walks its inherited_values chain — and read-only: it is a
|
|
40
|
+
# view, not the store, so writing to it registers nothing. Every
|
|
41
|
+
# semantic key has a dedicated accessor below; new code should use those.
|
|
42
|
+
def namespace_stackable
|
|
43
|
+
StackableValues.new(@stackable_values, parent&.namespace_stackable || {})
|
|
18
44
|
end
|
|
19
45
|
|
|
20
46
|
# Retrieve global settings.
|
|
@@ -34,13 +60,22 @@ module Grape
|
|
|
34
60
|
# #inherit_from).
|
|
35
61
|
def initialize
|
|
36
62
|
@route = {}
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@
|
|
63
|
+
# Namespace settings are scope-local by design: nothing ever layers
|
|
64
|
+
# them over a parent's (see #inherit_from, and the nesting behaviour
|
|
65
|
+
# DSL::Settings#namespace_setting is specified to have), so a plain
|
|
66
|
+
# Hash is the whole store.
|
|
67
|
+
@namespace = {}
|
|
68
|
+
# This scope's own inheritable overrides. Like @stackable_values it
|
|
69
|
+
# stays nil until the first write, and inheritance is resolved by
|
|
70
|
+
# walking #parent (see #inheritable) rather than by keeping a second
|
|
71
|
+
# chain of stores alongside it.
|
|
72
|
+
@namespace_inheritable = nil
|
|
73
|
+
# This scope's own stackable registrations, one Array per key. Stays
|
|
74
|
+
# nil until the first registration so scopes that only inherit don't
|
|
75
|
+
# each carry an empty Hash.
|
|
76
|
+
@stackable_values = nil
|
|
42
77
|
@parent = nil
|
|
43
|
-
|
|
78
|
+
@point_in_time_copies = nil
|
|
44
79
|
end
|
|
45
80
|
|
|
46
81
|
# Return the class-level global properties.
|
|
@@ -48,18 +83,15 @@ module Grape
|
|
|
48
83
|
self.class.global
|
|
49
84
|
end
|
|
50
85
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# from us.
|
|
86
|
+
# Inherit from the given parent: its values resolve behind ours from now
|
|
87
|
+
# on, including any it gains later. Also re-parents any settings
|
|
88
|
+
# instances which were forked from us.
|
|
54
89
|
# @param parent [InheritableSetting]
|
|
55
90
|
def inherit_from(parent)
|
|
56
91
|
return if parent.nil?
|
|
57
92
|
|
|
58
93
|
@parent = parent
|
|
59
94
|
|
|
60
|
-
namespace_inheritable.inherited_values = parent.namespace_inheritable
|
|
61
|
-
namespace_stackable.inherited_values = parent.namespace_stackable
|
|
62
|
-
namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable
|
|
63
95
|
@route = parent.route.merge(route)
|
|
64
96
|
|
|
65
97
|
@point_in_time_copies&.each { |cloned_one| cloned_one.inherit_from parent }
|
|
@@ -71,53 +103,735 @@ module Grape
|
|
|
71
103
|
# which were made.
|
|
72
104
|
def point_in_time_copy
|
|
73
105
|
new_setting = self.class.new
|
|
74
|
-
point_in_time_copies << new_setting
|
|
106
|
+
(@point_in_time_copies ||= []) << new_setting
|
|
75
107
|
new_setting.copy_state_from(self)
|
|
76
108
|
new_setting.inherit_from(parent)
|
|
77
109
|
new_setting
|
|
78
110
|
end
|
|
79
111
|
|
|
112
|
+
# Fork a point-in-time copy prepared for a freshly-built endpoint: the
|
|
113
|
+
# declared params and validations accumulated by the surrounding scopes
|
|
114
|
+
# are snapshotted into the copy's per-route settings, since the
|
|
115
|
+
# namespace stacks are wiped between routes (see #reset_validations!),
|
|
116
|
+
# and request-serving defaults are applied.
|
|
117
|
+
def point_in_time_copy_for_endpoint
|
|
118
|
+
copy = point_in_time_copy
|
|
119
|
+
copy.route_declared_params = copy.declared_params.flatten
|
|
120
|
+
copy.route_validations = copy.validations.dup
|
|
121
|
+
copy.default_error_status ||= 500
|
|
122
|
+
copy
|
|
123
|
+
end
|
|
124
|
+
|
|
80
125
|
# Resets the instance store of per-route settings.
|
|
81
126
|
# @api private
|
|
82
127
|
def route_end
|
|
83
128
|
@route = {}
|
|
84
129
|
end
|
|
85
130
|
|
|
131
|
+
# Validator instances and declared-params entries for the route currently
|
|
132
|
+
# being built. Unlike the same-named namespace stacks (#validations /
|
|
133
|
+
# #declared_params), these are flat per-route snapshots: seeded when an
|
|
134
|
+
# endpoint copy is forked (see #point_in_time_copy_for_endpoint), topped
|
|
135
|
+
# up from mounting parents (see Endpoint#inherit_settings), and read back
|
|
136
|
+
# by run_validators / #declared.
|
|
137
|
+
def route_validations
|
|
138
|
+
@route[:validations]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def route_validations=(validations)
|
|
142
|
+
@route[:validations] = validations
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def route_declared_params
|
|
146
|
+
@route[:declared_params]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def route_declared_params=(declared_params)
|
|
150
|
+
@route[:declared_params] = declared_params
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Path => renamed-name map recorded by +as:+ (see ParamsScope), consumed
|
|
154
|
+
# by #declared. Record entries with #add_route_renamed_param; an empty
|
|
155
|
+
# Hash when nothing was renamed.
|
|
156
|
+
def route_renamed_params
|
|
157
|
+
@route[:renamed_params] || {}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_route_renamed_param(path, new_name)
|
|
161
|
+
(@route[:renamed_params] ||= {})[path] = new_name
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Endpoint description recorded by +desc+ (see DSL::Desc), consumed by
|
|
165
|
+
# +route+. An empty Hash when +desc+ was never called.
|
|
166
|
+
def route_description
|
|
167
|
+
@route[:description] || {}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def route_description=(description)
|
|
171
|
+
@route[:description] = description
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The route-scope settings handed to each Grape::Router::Route: every
|
|
175
|
+
# +route_setting+ registration plus the description, minus the internal
|
|
176
|
+
# param snapshots (#route_validations / #route_declared_params).
|
|
177
|
+
def route_settings
|
|
178
|
+
route.except(:declared_params, :validations)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Read (when +value+ is nil) or write an arbitrary route-scoped setting.
|
|
182
|
+
# This is the open store behind the +route_setting+ DSL; the known keys
|
|
183
|
+
# have the dedicated accessors above.
|
|
184
|
+
def route_setting(key, value = nil)
|
|
185
|
+
return @route[key] if value.nil?
|
|
186
|
+
|
|
187
|
+
@route[key] = value
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Fold a mounting parent scope's accumulated validations and declared
|
|
191
|
+
# params into this endpoint copy's per-route snapshots (see
|
|
192
|
+
# Endpoint#inherit_settings). Both are appended, so the parent's entries
|
|
193
|
+
# follow the ones already seeded from the surrounding scopes.
|
|
194
|
+
def inherit_route_params(parent)
|
|
195
|
+
parent_validations = parent.validations
|
|
196
|
+
route_validations.concat(parent_validations) if parent_validations.any?
|
|
197
|
+
|
|
198
|
+
parent_declared_params = parent.declared_params
|
|
199
|
+
route_declared_params.concat(parent_declared_params.flatten) if parent_declared_params.any?
|
|
200
|
+
end
|
|
201
|
+
|
|
86
202
|
# Return a serializable hash of our values.
|
|
87
203
|
def to_hash
|
|
88
204
|
{
|
|
89
205
|
global: global.clone,
|
|
90
206
|
route: route.clone,
|
|
91
|
-
namespace: namespace.
|
|
92
|
-
namespace_inheritable:
|
|
93
|
-
namespace_stackable:
|
|
94
|
-
|
|
207
|
+
namespace: namespace.dup,
|
|
208
|
+
namespace_inheritable: inheritable_values,
|
|
209
|
+
namespace_stackable: stacked_keys.to_h { |key| [key, stacked(key)] },
|
|
210
|
+
rescue_handlers:,
|
|
211
|
+
base_only_rescue_handlers:
|
|
95
212
|
}
|
|
96
213
|
end
|
|
97
214
|
|
|
98
215
|
def ==(other)
|
|
99
|
-
|
|
216
|
+
return true if equal?(other)
|
|
217
|
+
return false unless other.is_a?(self.class)
|
|
218
|
+
|
|
219
|
+
# Endpoint copies are siblings of the scope they were forked from, not
|
|
220
|
+
# children of it (see #point_in_time_copy), so every endpoint of an API
|
|
221
|
+
# hangs off the same parent object — which is the case the duplicate
|
|
222
|
+
# check in DSL::Routing#route runs on. Both sides then inherit the same
|
|
223
|
+
# values, so comparing own state decides it without serializing either
|
|
224
|
+
# chain; #global is class-level and identical for both either way.
|
|
225
|
+
# Stacks concatenate and #stack never records an empty one, so matching
|
|
226
|
+
# own stacks means matching resolved ones; the rescue handler maps
|
|
227
|
+
# merge, where a scope can restate an inherited mapping, so those are
|
|
228
|
+
# compared resolved.
|
|
229
|
+
return to_hash == other.to_hash unless parent.equal?(other.parent)
|
|
230
|
+
|
|
231
|
+
same_own_store?(@stackable_values, other.stackable_values) &&
|
|
232
|
+
route == other.route &&
|
|
233
|
+
@namespace_inheritable == other.namespace_inheritable &&
|
|
234
|
+
namespace == other.namespace &&
|
|
235
|
+
rescue_handlers == other.rescue_handlers &&
|
|
236
|
+
base_only_rescue_handlers == other.base_only_rescue_handlers
|
|
100
237
|
end
|
|
101
238
|
alias eql? ==
|
|
102
239
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
240
|
+
# Keyed on the fully resolved state, because #== accepts two instances
|
|
241
|
+
# whose own stores differ as long as their chains resolve alike (the
|
|
242
|
+
# #to_hash path above) — hashing own state would tell those apart. The
|
|
243
|
+
# same-parent fast path implies equal resolved state, so it agrees. This
|
|
244
|
+
# is the cold path: nothing in Grape uses a setting as a Hash key or in
|
|
245
|
+
# a Set, and #== keeps avoiding #to_hash wherever it can.
|
|
246
|
+
def hash
|
|
247
|
+
to_hash.hash
|
|
248
|
+
end
|
|
106
249
|
|
|
107
|
-
|
|
250
|
+
# Validator instances registered by +params+ and +contract+ blocks,
|
|
251
|
+
# outermost scope first. Record them with #add_validation; the backing
|
|
252
|
+
# store is an internal detail.
|
|
253
|
+
def validations
|
|
254
|
+
stacked(:validations)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def add_validation(validator)
|
|
258
|
+
stack(:validations, validator)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Declared-params entries registered by +params+ blocks, one Array per
|
|
262
|
+
# scope, outermost scope first. Record them with #add_declared_params;
|
|
263
|
+
# the backing store is an internal detail.
|
|
264
|
+
def declared_params
|
|
265
|
+
stacked(:declared_params)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def add_declared_params(params)
|
|
269
|
+
stack(:declared_params, params)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Param documentation recorded by +params+ blocks (see
|
|
273
|
+
# Validations::ParamsDocumentation) as one attribute-name => details
|
|
274
|
+
# Hash per scope, deep-merged on read; nil when nothing is documented.
|
|
275
|
+
# Record entries with #add_params_documentation; the backing store is
|
|
276
|
+
# an internal detail.
|
|
277
|
+
def params_documentation
|
|
278
|
+
namespace_stackable_with_hash(:params)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def add_params_documentation(documented_attrs)
|
|
282
|
+
stack(:params, documented_attrs)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Drops this scope's own validations, declared params and params
|
|
286
|
+
# documentation once an endpoint has consumed them (see
|
|
287
|
+
# +reset_validations!+ in DSL::Validations). Inherited entries are kept.
|
|
288
|
+
def reset_validations!
|
|
289
|
+
unstack(:declared_params, :params, :validations)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Reusable +params :name do ... end+ blocks defined in helpers, as one
|
|
293
|
+
# name => block Hash per scope, deep-merged on read; nil when none are
|
|
294
|
+
# defined. Consumed by +use+. Record entries with #add_named_params;
|
|
295
|
+
# the backing store is an internal detail.
|
|
296
|
+
def named_params
|
|
297
|
+
namespace_stackable_with_hash(:named_params)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def add_named_params(named_params)
|
|
301
|
+
stack(:named_params, named_params)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Filter blocks registered by the callbacks DSL (see DSL::Callbacks),
|
|
305
|
+
# as a callback-name => blocks Array Hash keyed by the DSL method names
|
|
306
|
+
# (+:before+, +:before_validation+, +:after_validation+, +:after+,
|
|
307
|
+
# +:finally+), outermost scope first. Record them with #add_callback;
|
|
308
|
+
# the backing store is an internal detail.
|
|
309
|
+
def callbacks
|
|
310
|
+
CALLBACK_STORE_KEYS.transform_values { |store_key| stacked(store_key) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def add_callback(callback_name, block)
|
|
314
|
+
stack(CALLBACK_STORE_KEYS.fetch(callback_name), block)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Response-shaping options recorded by +rescue_from+ (see
|
|
318
|
+
# DSL::RescueOptions): every +rescue_from+ stacks one entry and the
|
|
319
|
+
# nearest scope's latest registration wins on read; nil when
|
|
320
|
+
# +rescue_from+ was never called. Record them with #add_rescue_options;
|
|
321
|
+
# the backing store is an internal detail.
|
|
322
|
+
def rescue_options
|
|
323
|
+
stacked(:rescue_options).last
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def add_rescue_options(options)
|
|
327
|
+
stack(:rescue_options, options)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Meta-selector registrations from +rescue_from :all+,
|
|
331
|
+
# +:grape_exceptions+ and +:internal_grape_exceptions+ (see
|
|
332
|
+
# DSL::RequestResponse#rescue_from): each records its handler (nil to
|
|
333
|
+
# use the built-in one) and flips the flags the error middleware reads
|
|
334
|
+
# through #rescue_all? / #rescue_grape_exceptions?; the backing store
|
|
335
|
+
# is an internal detail.
|
|
336
|
+
def add_all_rescue_handler(handler)
|
|
337
|
+
set_inheritable(:rescue_all, true)
|
|
338
|
+
set_inheritable(:all_rescue_handler, handler)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def add_grape_exceptions_rescue_handler(handler)
|
|
342
|
+
set_inheritable(:rescue_all, true)
|
|
343
|
+
set_inheritable(:rescue_grape_exceptions, true)
|
|
344
|
+
set_inheritable(:grape_exceptions_rescue_handler, handler)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def add_internal_grape_exceptions_rescue_handler(handler)
|
|
348
|
+
set_inheritable(:internal_grape_exceptions_rescue_handler, handler)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def rescue_all?
|
|
352
|
+
inheritable(:rescue_all) == true
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def rescue_grape_exceptions?
|
|
356
|
+
inheritable(:rescue_grape_exceptions) == true
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def all_rescue_handler
|
|
360
|
+
inheritable(:all_rescue_handler)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def grape_exceptions_rescue_handler
|
|
364
|
+
inheritable(:grape_exceptions_rescue_handler)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def internal_grape_exceptions_rescue_handler
|
|
368
|
+
inheritable(:internal_grape_exceptions_rescue_handler)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Rescue-handler maps registered by +rescue_from+, keyed by exception
|
|
372
|
+
# class and merged so a nested scope's handler wins. Record them with
|
|
373
|
+
# #add_rescue_handlers; the backing store is an internal detail.
|
|
374
|
+
def rescue_handlers
|
|
375
|
+
merged_rescue_handlers(:rescue_handlers)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def base_only_rescue_handlers
|
|
379
|
+
merged_rescue_handlers(:base_only_rescue_handlers)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# An exception class registered twice in the same scope keeps its first
|
|
383
|
+
# handler, and keeps the position it was first registered at.
|
|
384
|
+
def add_rescue_handlers(mapping, subclasses:)
|
|
385
|
+
@rescue_handler_maps ||= {}
|
|
386
|
+
own = (@rescue_handler_maps[subclasses ? :rescue_handlers : :base_only_rescue_handlers] ||= {})
|
|
387
|
+
ShadowedRescueHandlers.warn_about(own, mapping) if subclasses
|
|
388
|
+
own.merge!(mapping) { |_klass, registered, _new| registered }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Content negotiation registries recorded by the request/response DSL
|
|
392
|
+
# (see DSL::RequestResponse): the content-type registry (+content_type+
|
|
393
|
+
# and +format+), and the formatter, parser and error-formatter handler
|
|
394
|
+
# maps. Each registration stacks one single-entry Hash, deep-merged on
|
|
395
|
+
# read so a nested scope's registration wins; readers return nil when
|
|
396
|
+
# nothing is registered. Record entries with the corresponding +add_*+
|
|
397
|
+
# writer; the backing store is an internal detail.
|
|
398
|
+
def content_types
|
|
399
|
+
namespace_stackable_with_hash(:content_types)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def add_content_type(format, content_type)
|
|
403
|
+
stack(:content_types, { format => content_type })
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def formatters
|
|
407
|
+
namespace_stackable_with_hash(:formatters)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def add_formatter(content_type, formatter)
|
|
411
|
+
stack(:formatters, { content_type => formatter })
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def parsers
|
|
415
|
+
namespace_stackable_with_hash(:parsers)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def add_parser(content_type, parser)
|
|
419
|
+
stack(:parsers, { content_type => parser })
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def error_formatters
|
|
423
|
+
namespace_stackable_with_hash(:error_formatters)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def add_error_formatter(format, formatter)
|
|
427
|
+
stack(:error_formatters, { format => formatter })
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Model-class => entity-class registrations from +represent+ (see
|
|
431
|
+
# DSL::RequestResponse), one single-entry Hash per registration,
|
|
432
|
+
# deep-merged on read so a nested scope's registration wins; nil when
|
|
433
|
+
# none are registered. Record them with #add_representation; the
|
|
434
|
+
# backing store is an internal detail.
|
|
435
|
+
def representations
|
|
436
|
+
namespace_stackable_with_hash(:representations)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def add_representation(model_class, entity_class)
|
|
440
|
+
stack(:representations, { model_class => entity_class })
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Middleware specs recorded by the middleware DSL (+use+, +insert+,
|
|
444
|
+
# +insert_before+, +insert_after+; see DSL::Middleware), one
|
|
445
|
+
# [operation, *arguments] Array per registration, outermost scope
|
|
446
|
+
# first. Record them with #add_middleware; the backing store is an
|
|
447
|
+
# internal detail.
|
|
448
|
+
def middleware
|
|
449
|
+
stacked(:middleware)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def add_middleware(operation_with_arguments)
|
|
453
|
+
stack(:middleware, operation_with_arguments)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Helper modules registered by +helpers+ blocks and modules (see
|
|
457
|
+
# DSL::Helpers), outermost scope first. Record them with #add_helper;
|
|
458
|
+
# the backing store is an internal detail.
|
|
459
|
+
def helpers
|
|
460
|
+
stacked(:helpers)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def add_helper(mod)
|
|
464
|
+
stack(:helpers, mod)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Grape::Namespace objects registered by the +namespace+ DSL and its
|
|
468
|
+
# aliases (group, resource, resources, segment; see DSL::Routing),
|
|
469
|
+
# outermost scope first. Not to be confused with the #namespace values
|
|
470
|
+
# store. Record them with #add_namespace; the backing store is an
|
|
471
|
+
# internal detail.
|
|
472
|
+
def namespaces
|
|
473
|
+
stacked(:namespace)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def add_namespace(namespace)
|
|
477
|
+
stack(:namespace, namespace)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# The normalized path prefix formed by joining every registered
|
|
481
|
+
# namespace's space (see Grape::Namespace.joined_space_path).
|
|
482
|
+
def namespace_path
|
|
483
|
+
Grape::Namespace.joined_space_path(namespaces)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# The param requirements declared by registered namespaces, outermost
|
|
487
|
+
# scope first.
|
|
488
|
+
def namespace_requirements
|
|
489
|
+
namespaces.filter_map(&:requirements)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# The path a Grape API is mounted under, recorded on the mounted API's
|
|
493
|
+
# top-level settings by +mount+ (see DSL::Routing). Reading returns the
|
|
494
|
+
# outermost mount path — nil when the API is not mounted; the backing
|
|
495
|
+
# store is an internal detail.
|
|
496
|
+
def mount_path
|
|
497
|
+
stacked(:mount_path).first
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def add_mount_path(mount_path)
|
|
501
|
+
stack(:mount_path, mount_path)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The full mount-path stack — one entry per mount level, outermost
|
|
505
|
+
# first; what Router::Pattern::Path joins into a route's origin (see
|
|
506
|
+
# #path_settings).
|
|
507
|
+
def mount_paths
|
|
508
|
+
stacked(:mount_path)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Dry::Schema key maps registered by +contract+ blocks (see
|
|
512
|
+
# Validations::ContractScope), one per contract, outermost scope first;
|
|
513
|
+
# +declared+ uses them to write coerced params back under their
|
|
514
|
+
# declared keys. Record them with #add_contract_key_map; the backing
|
|
515
|
+
# store is an internal detail.
|
|
516
|
+
def contract_key_maps
|
|
517
|
+
stacked(:contract_key_map)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def add_contract_key_map(key_map)
|
|
521
|
+
stack(:contract_key_map, key_map)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Serialization and error-response defaults recorded by the
|
|
525
|
+
# request/response DSL's get-or-set methods (see DSL::RequestResponse):
|
|
526
|
+
# +format+ is the enforced API format, +default_format+ the fallback
|
|
527
|
+
# used when a request doesn't specify one, and
|
|
528
|
+
# +default_error_formatter+ / +default_error_status+ shape error
|
|
529
|
+
# responses. Nearest-wins scalars — a nested scope's assignment
|
|
530
|
+
# overrides an inherited one, hence plain +=+ writers rather than the
|
|
531
|
+
# +add_*+ writers used for stackable registrations. Readers return nil
|
|
532
|
+
# when never set (Endpoint applies the request-serving fallbacks); the
|
|
533
|
+
# backing store is an internal detail.
|
|
534
|
+
def format
|
|
535
|
+
inheritable(:format)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def format=(format)
|
|
539
|
+
set_inheritable(:format, format)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def default_format
|
|
543
|
+
inheritable(:default_format)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def default_format=(default_format)
|
|
547
|
+
set_inheritable(:default_format, default_format)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def default_error_formatter
|
|
551
|
+
inheritable(:default_error_formatter)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def default_error_formatter=(formatter)
|
|
555
|
+
set_inheritable(:default_error_formatter, formatter)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def default_error_status
|
|
559
|
+
inheritable(:default_error_status)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def default_error_status=(status)
|
|
563
|
+
set_inheritable(:default_error_status, status)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# Versioning state recorded by the routing DSL (see DSL::Routing):
|
|
567
|
+
# +version+ holds the Array of version strings registered by the
|
|
568
|
+
# +version+ DSL method, +version_options+ its DSL::VersionOptions
|
|
569
|
+
# value object, and +root_prefix+ the path prefix set by +prefix+.
|
|
570
|
+
# Nearest-wins scalars with plain += writers; readers return nil when
|
|
571
|
+
# never set; the backing store is an internal detail.
|
|
572
|
+
def version
|
|
573
|
+
inheritable(:version)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def version=(versions)
|
|
577
|
+
set_inheritable(:version, versions)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def version_options
|
|
581
|
+
inheritable(:version_options)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def version_options=(options)
|
|
585
|
+
set_inheritable(:version_options, options)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def root_prefix
|
|
589
|
+
inheritable(:root_prefix)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def root_prefix=(prefix)
|
|
593
|
+
set_inheritable(:root_prefix, prefix)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Cascade flag assigned by the +cascade+ DSL. An explicit nil is
|
|
597
|
+
# meaningful and distinct from never-set (the backing store is
|
|
598
|
+
# key-presence based), so #cascade_defined? reports whether any scope
|
|
599
|
+
# assigned it — Grape::API::Instance#cascade? falls back to the
|
|
600
|
+
# version options' cascade, then to true, when it was never assigned.
|
|
601
|
+
def cascade
|
|
602
|
+
inheritable(:cascade)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def cascade=(value)
|
|
606
|
+
set_inheritable(:cascade, value)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def cascade_defined?
|
|
610
|
+
inheritable?(:cascade)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Scope flags flipped by the routing DSL's bang methods (see
|
|
614
|
+
# DSL::Routing#do_not_route_head! and friends; Validations::OneofCollector
|
|
615
|
+
# also flips +do_not_document!+): once set in a scope they apply to it
|
|
616
|
+
# and everything nested under it. Readers return false when never set;
|
|
617
|
+
# the backing store is an internal detail.
|
|
618
|
+
def do_not_route_head!
|
|
619
|
+
set_inheritable(:do_not_route_head, true)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def do_not_route_head?
|
|
623
|
+
inheritable(:do_not_route_head) == true
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def do_not_route_options!
|
|
627
|
+
set_inheritable(:do_not_route_options, true)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def do_not_route_options?
|
|
631
|
+
inheritable(:do_not_route_options) == true
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def do_not_document!
|
|
635
|
+
set_inheritable(:do_not_document, true)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def do_not_document?
|
|
639
|
+
inheritable(:do_not_document) == true
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def lint!
|
|
643
|
+
set_inheritable(:lint, true)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def lint?
|
|
647
|
+
inheritable(:lint) == true
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# The params-builder strategy set by +build_with+ (both the
|
|
651
|
+
# API-level DSL::Routing#build_with and the params-block
|
|
652
|
+
# DSL::Parameters#build_with write it), consumed when the endpoint
|
|
653
|
+
# builds its Grape::Request. Nearest-wins scalar; nil when never set;
|
|
654
|
+
# the backing store is an internal detail.
|
|
655
|
+
def build_params_with
|
|
656
|
+
inheritable(:build_params_with)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def build_params_with=(strategy)
|
|
660
|
+
set_inheritable(:build_params_with, strategy)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# The authentication configuration Hash recorded by the +auth+ DSL
|
|
664
|
+
# (see Middleware::Auth::DSL): {type:, proc:, **options}. Nearest-wins
|
|
665
|
+
# scalar; nil when no authenticator is declared — Endpoint uses that
|
|
666
|
+
# to warn about unauthenticated bare Rack mounts; the backing store is
|
|
667
|
+
# an internal detail.
|
|
668
|
+
def auth
|
|
669
|
+
inheritable(:auth)
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def auth=(auth_options)
|
|
673
|
+
set_inheritable(:auth, auth_options)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Immutable snapshot of the settings Router::Pattern::Path reads to
|
|
677
|
+
# assemble a route's origin and suffix, built by #path_settings, which
|
|
678
|
+
# always supplies every member (nil where unset) — unlike
|
|
679
|
+
# RescueOptions/VersionOptions, PathSettings has no bare-default
|
|
680
|
+
# production caller, so it stays a plain Data with no keyword
|
|
681
|
+
# defaults.
|
|
682
|
+
PathSettings = Data.define(:mount_path, :root_prefix, :format, :content_types, :version, :version_options)
|
|
683
|
+
|
|
684
|
+
# Builds a PathSettings snapshot for Router::Pattern::Path (see
|
|
685
|
+
# Endpoint#to_routes). +mount_path+ is the full stack — one entry per
|
|
686
|
+
# mount level, outermost first — unlike #mount_path, which returns
|
|
687
|
+
# only the outermost entry; +content_types+ is the raw registration
|
|
688
|
+
# stack, because Path counts registrations rather than distinct
|
|
689
|
+
# formats. Unset members are nil.
|
|
690
|
+
def path_settings
|
|
691
|
+
PathSettings.new(
|
|
692
|
+
mount_path: mount_paths.presence,
|
|
693
|
+
root_prefix:,
|
|
694
|
+
format:,
|
|
695
|
+
content_types: stacked(:content_types).presence,
|
|
696
|
+
version:,
|
|
697
|
+
version_options:
|
|
698
|
+
)
|
|
108
699
|
end
|
|
109
700
|
|
|
110
701
|
protected
|
|
111
702
|
|
|
703
|
+
# This scope's own inheritable overrides, before inheritance; nil when
|
|
704
|
+
# the scope overrode nothing. Peer access for #copy_state_from.
|
|
705
|
+
attr_reader :namespace_inheritable
|
|
706
|
+
|
|
707
|
+
# The nearest scope's value for +key+: this scope's own override when it
|
|
708
|
+
# has one, otherwise the enclosing scope's. Keyed on presence rather than
|
|
709
|
+
# truthiness, so a scope can deliberately override an inherited value
|
|
710
|
+
# with nil (see #cascade).
|
|
711
|
+
def inheritable(key)
|
|
712
|
+
return @namespace_inheritable[key] if @namespace_inheritable&.key?(key)
|
|
713
|
+
|
|
714
|
+
parent&.inheritable(key)
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Whether any scope along the chain assigned +key+ — including one that
|
|
718
|
+
# assigned nil, which #inheritable cannot distinguish from never-set.
|
|
719
|
+
def inheritable?(key)
|
|
720
|
+
return true if @namespace_inheritable&.key?(key)
|
|
721
|
+
|
|
722
|
+
parent&.inheritable?(key) || false
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Every inheritable value along the chain resolved into one Hash, nearest
|
|
726
|
+
# scope winning. Like #stacked it hands back the backing Hash when only
|
|
727
|
+
# one scope in the chain has values, so callers must treat the result as
|
|
728
|
+
# read-only.
|
|
729
|
+
def inheritable_values
|
|
730
|
+
inherited = parent&.inheritable_values
|
|
731
|
+
own = @namespace_inheritable
|
|
732
|
+
return own || {} unless inherited
|
|
733
|
+
|
|
734
|
+
own ? inherited.merge(own) : inherited
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# This scope's own +rescue_from+ registrations, before inheritance:
|
|
738
|
+
# {rescue_handlers: {klass => handler}, base_only_rescue_handlers: {...}}.
|
|
739
|
+
attr_reader :rescue_handler_maps
|
|
740
|
+
|
|
741
|
+
# This scope's own stackable registrations, before inheritance; nil when
|
|
742
|
+
# the scope registered nothing. Peer access for #copy_state_from.
|
|
743
|
+
attr_reader :stackable_values
|
|
744
|
+
|
|
745
|
+
# Every registration for +key+ along the chain, outermost scope first.
|
|
746
|
+
# Returns the frozen EMPTY_STACK when nothing is registered anywhere, and
|
|
747
|
+
# — like the store it replaced — the backing Array itself when only this
|
|
748
|
+
# scope registered anything, so callers must treat the result as
|
|
749
|
+
# read-only.
|
|
750
|
+
def stacked(key)
|
|
751
|
+
inherited = parent&.stacked(key)
|
|
752
|
+
own = @stackable_values&.[](key)
|
|
753
|
+
return own || EMPTY_STACK unless inherited
|
|
754
|
+
|
|
755
|
+
own ? inherited + own : inherited
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# Every key registered along the chain, outermost scope's keys first.
|
|
759
|
+
def stacked_keys
|
|
760
|
+
inherited = parent&.stacked_keys || EMPTY_STACK
|
|
761
|
+
return inherited if @stackable_values.blank?
|
|
762
|
+
|
|
763
|
+
(inherited + @stackable_values.keys).uniq
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Nearest scope's handlers first: Middleware::Error scans with +find+,
|
|
767
|
+
# so a nested scope's registrations must precede inherited ones even
|
|
768
|
+
# when an outer scope registered a more specific class.
|
|
769
|
+
def merged_rescue_handlers(key)
|
|
770
|
+
inherited = parent&.merged_rescue_handlers(key)
|
|
771
|
+
own = @rescue_handler_maps&.[](key)
|
|
772
|
+
return inherited unless own
|
|
773
|
+
|
|
774
|
+
own.merge(inherited || {}) { |_klass, nearer, _inherited| nearer }
|
|
775
|
+
end
|
|
776
|
+
|
|
112
777
|
# Used by +point_in_time_copy+ to populate a freshly-built instance
|
|
113
778
|
# with cloned state from another instance of the same class.
|
|
114
779
|
def copy_state_from(source)
|
|
115
|
-
@namespace = source.namespace.
|
|
116
|
-
@namespace_inheritable = source.namespace_inheritable
|
|
117
|
-
|
|
118
|
-
|
|
780
|
+
@namespace = source.namespace.dup
|
|
781
|
+
@namespace_inheritable = source.namespace_inheritable&.dup
|
|
782
|
+
# The nested stores are duped too, not just the Hash holding them. A
|
|
783
|
+
# copy is a point in time: what the source registers afterwards must not
|
|
784
|
+
# reach it. Sharing them made that depend on whether the key already
|
|
785
|
+
# held a registration — `use` below a route applied to it when some
|
|
786
|
+
# earlier `use` had seeded the Array, and did nothing otherwise; the
|
|
787
|
+
# same went for `rescue_from` below a route.
|
|
788
|
+
@stackable_values = source.stackable_values&.transform_values(&:dup)
|
|
789
|
+
@rescue_handler_maps = source.rescue_handler_maps&.transform_values(&:dup)
|
|
119
790
|
@route = source.route.clone
|
|
120
|
-
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
private
|
|
794
|
+
|
|
795
|
+
# Overrides +key+ for this scope, leaving the enclosing scopes' value
|
|
796
|
+
# untouched. The store is allocated on first use. Returns the assigned
|
|
797
|
+
# value, since the writers built on it are +=+ methods.
|
|
798
|
+
def set_inheritable(key, value)
|
|
799
|
+
(@namespace_inheritable ||= {})[key] = value
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Appends one registration for +key+ to this scope, leaving inherited
|
|
803
|
+
# ones untouched. The store is allocated on first use. Returns the
|
|
804
|
+
# registered value, since this replaced an assignment expression and the
|
|
805
|
+
# +add_*+ writers built on it inherited that return value.
|
|
806
|
+
def stack(key, value)
|
|
807
|
+
((@stackable_values ||= {})[key] ||= []) << value
|
|
808
|
+
value
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Drops this scope's own registrations for +keys+; inherited ones are
|
|
812
|
+
# kept, since they belong to the enclosing scopes.
|
|
813
|
+
def unstack(*keys)
|
|
814
|
+
return if @stackable_values.nil?
|
|
815
|
+
|
|
816
|
+
keys.each { |key| @stackable_values.delete(key) }
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# Compares two lazily-allocated own-registration stores (see #stack and
|
|
820
|
+
# #add_rescue_handlers): nil and an emptied Hash both mean "this scope
|
|
821
|
+
# registered nothing", so #== must not tell them apart.
|
|
822
|
+
def same_own_store?(mine, theirs)
|
|
823
|
+
return theirs.blank? if mine.blank?
|
|
824
|
+
|
|
825
|
+
mine == theirs
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Deep-merges a stackable key's registrations into one Hash, nearest
|
|
829
|
+
# scope winning; nil when nothing is registered.
|
|
830
|
+
def namespace_stackable_with_hash(key)
|
|
831
|
+
data = stacked(key)
|
|
832
|
+
return if data.blank?
|
|
833
|
+
|
|
834
|
+
data.each_with_object({}) { |value, result| result.deep_merge!(value) }
|
|
121
835
|
end
|
|
122
836
|
end
|
|
123
837
|
end
|