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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/CONTRIBUTING.md +16 -0
  4. data/README.md +208 -8
  5. data/UPGRADING.md +756 -6
  6. data/lib/grape/api/instance.rb +40 -33
  7. data/lib/grape/content_types.rb +47 -1
  8. data/lib/grape/dry_types.rb +1 -1
  9. data/lib/grape/dsl/callbacks.rb +3 -9
  10. data/lib/grape/dsl/declared.rb +4 -4
  11. data/lib/grape/dsl/desc.rb +18 -8
  12. data/lib/grape/dsl/entity.rb +37 -19
  13. data/lib/grape/dsl/helpers.rb +3 -3
  14. data/lib/grape/dsl/inside_route.rb +20 -8
  15. data/lib/grape/dsl/middleware.rb +3 -3
  16. data/lib/grape/dsl/parameters.rb +54 -28
  17. data/lib/grape/dsl/request_response.rb +37 -39
  18. data/lib/grape/dsl/rescue_options.rb +6 -5
  19. data/lib/grape/dsl/routing.rb +116 -49
  20. data/lib/grape/dsl/settings.rb +1 -1
  21. data/lib/grape/dsl/validations.rb +3 -3
  22. data/lib/grape/dsl/version_options.rb +3 -3
  23. data/lib/grape/endpoint/options.rb +13 -7
  24. data/lib/grape/endpoint.rb +141 -118
  25. data/lib/grape/env.rb +7 -0
  26. data/lib/grape/error_formatter/base.rb +6 -5
  27. data/lib/grape/error_formatter.rb +6 -2
  28. data/lib/grape/exceptions/error_response.rb +4 -1
  29. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  30. data/lib/grape/exceptions/validation.rb +9 -8
  31. data/lib/grape/formatter/json.rb +1 -0
  32. data/lib/grape/formatter/serializable_hash.rb +1 -0
  33. data/lib/grape/locale/en.yml +1 -0
  34. data/lib/grape/middleware/auth/dsl.rb +5 -33
  35. data/lib/grape/middleware/error.rb +153 -46
  36. data/lib/grape/middleware/formatter.rb +95 -26
  37. data/lib/grape/middleware/precomputed_content_types.rb +12 -7
  38. data/lib/grape/middleware/stack.rb +13 -4
  39. data/lib/grape/middleware/versioner/base.rb +3 -9
  40. data/lib/grape/middleware/versioner/path.rb +49 -7
  41. data/lib/grape/namespace.rb +3 -2
  42. data/lib/grape/path.rb +8 -69
  43. data/lib/grape/precompiled_json.rb +50 -0
  44. data/lib/grape/request.rb +36 -4
  45. data/lib/grape/router/base_route.rb +47 -10
  46. data/lib/grape/router/greedy_route.rb +5 -1
  47. data/lib/grape/router/pattern/path.rb +78 -0
  48. data/lib/grape/router/pattern.rb +60 -14
  49. data/lib/grape/router/route.rb +62 -10
  50. data/lib/grape/router.rb +100 -57
  51. data/lib/grape/serve_stream/file_body.rb +6 -0
  52. data/lib/grape/serve_stream/stream_response.rb +6 -0
  53. data/lib/grape/util/api_description.rb +10 -1
  54. data/lib/grape/util/cache.rb +21 -2
  55. data/lib/grape/util/deep_freeze.rb +1 -2
  56. data/lib/grape/util/freeze_on_new.rb +20 -0
  57. data/lib/grape/util/inheritable_setting.rb +751 -37
  58. data/lib/grape/util/media_type.rb +10 -3
  59. data/lib/grape/util/path_normalizer.rb +12 -9
  60. data/lib/grape/util/registry.rb +12 -2
  61. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  62. data/lib/grape/util/stackable_values.rb +32 -6
  63. data/lib/grape/validations/attributes_iterator.rb +21 -4
  64. data/lib/grape/validations/contract_scope.rb +8 -7
  65. data/lib/grape/validations/oneof_collector.rb +6 -10
  66. data/lib/grape/validations/params_documentation.rb +2 -2
  67. data/lib/grape/validations/params_scope.rb +66 -26
  68. data/lib/grape/validations/single_attribute_iterator.rb +5 -3
  69. data/lib/grape/validations/types/array_coercer.rb +4 -6
  70. data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
  71. data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
  72. data/lib/grape/validations/types/json.rb +1 -3
  73. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  74. data/lib/grape/validations/types/primitive_coercer.rb +12 -9
  75. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
  76. data/lib/grape/validations/types.rb +5 -5
  77. data/lib/grape/validations/validations_spec.rb +14 -4
  78. data/lib/grape/validations/validators/base.rb +4 -7
  79. data/lib/grape/validations/validators/coerce_validator.rb +8 -2
  80. data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
  81. data/lib/grape/validations/validators/length_validator.rb +4 -2
  82. data/lib/grape/validations/validators/oneof_validator.rb +2 -0
  83. data/lib/grape/validations/validators/same_as_validator.rb +1 -0
  84. data/lib/grape/version.rb +2 -1
  85. data/lib/grape.rb +23 -2
  86. metadata +10 -9
  87. data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
  88. data/lib/grape/util/base_inheritable.rb +0 -43
  89. data/lib/grape/util/inheritable_values.rb +0 -33
  90. 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
- # A branchable, inheritable settings object which can store both stackable
6
- # and inheritable values (see InheritableValues and StackableValues).
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
- attr_reader :route, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent
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
- # Lazy-allocated; +api_class+ and +point_in_time_copies+ are rarely
11
- # written on most settings layers, so don't pay for a Hash/Array each.
12
- def api_class
13
- @api_class ||= {}
14
- end
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
- def point_in_time_copies
17
- @point_in_time_copies ||= []
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
- @namespace = InheritableValues.new # only inheritable from a parent when
38
- # used with a mount, or should every API::Class be a separate namespace by default?
39
- @namespace_inheritable = InheritableValues.new
40
- @namespace_stackable = StackableValues.new
41
- @namespace_reverse_stackable = ReverseStackableValues.new
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
- # @api_class and @point_in_time_copies stay nil until first access.
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
- # Set our inherited values to the given parent's current values. Also,
52
- # update the inherited values on any settings instances which were forked
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.to_hash,
92
- namespace_inheritable: namespace_inheritable.to_hash,
93
- namespace_stackable: namespace_stackable.to_hash,
94
- namespace_reverse_stackable: namespace_reverse_stackable.to_hash
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
- other.is_a?(self.class) && to_hash == other.to_hash
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
- def namespace_stackable_with_hash(key)
104
- data = namespace_stackable[key]
105
- return if data.blank?
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
- data.each_with_object({}) { |value, result| result.deep_merge!(value) }
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.clone
116
- @namespace_inheritable = source.namespace_inheritable.clone
117
- @namespace_stackable = source.namespace_stackable.clone
118
- @namespace_reverse_stackable = source.namespace_reverse_stackable.clone
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
- @api_class = source.api_class
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