grape 4.0.1 → 4.1.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/README.md +195 -95
  4. data/UPGRADING.md +364 -0
  5. data/grape.gemspec +1 -1
  6. data/lib/grape/api/instance.rb +57 -30
  7. data/lib/grape/api.rb +50 -20
  8. data/lib/grape/content_types.rb +26 -9
  9. data/lib/grape/cookies.rb +1 -1
  10. data/lib/grape/declared_params_handler.rb +35 -7
  11. data/lib/grape/dsl/declared.rb +5 -4
  12. data/lib/grape/dsl/desc.rb +1 -2
  13. data/lib/grape/dsl/entity.rb +35 -19
  14. data/lib/grape/dsl/headers.rb +3 -3
  15. data/lib/grape/dsl/helpers.rb +5 -8
  16. data/lib/grape/dsl/inside_route.rb +33 -27
  17. data/lib/grape/dsl/logger.rb +10 -4
  18. data/lib/grape/dsl/middleware.rb +4 -9
  19. data/lib/grape/dsl/mounting.rb +149 -0
  20. data/lib/grape/dsl/parameters.rb +78 -104
  21. data/lib/grape/dsl/request_response.rb +35 -22
  22. data/lib/grape/dsl/routing.rb +17 -191
  23. data/lib/grape/dsl/settings.rb +1 -1
  24. data/lib/grape/dsl/version_options.rb +2 -2
  25. data/lib/grape/dsl/versioning.rb +98 -0
  26. data/lib/grape/endpoint/state.rb +15 -0
  27. data/lib/grape/endpoint.rb +229 -137
  28. data/lib/grape/error_formatter/base.rb +28 -13
  29. data/lib/grape/error_formatter/json.rb +3 -0
  30. data/lib/grape/error_formatter/jsonl.rb +15 -0
  31. data/lib/grape/error_formatter/xml.rb +3 -1
  32. data/lib/grape/exceptions/error_response.rb +23 -7
  33. data/lib/grape/exceptions/halt.rb +39 -0
  34. data/lib/grape/exceptions/validation.rb +9 -9
  35. data/lib/grape/exceptions/validation_errors.rb +12 -2
  36. data/lib/grape/formatter/jsonl.rb +34 -0
  37. data/lib/grape/formatter/markdown.rb +17 -0
  38. data/lib/grape/formatter/md.rb +11 -0
  39. data/lib/grape/locale/en.yml +1 -0
  40. data/lib/grape/middleware/auth/base.rb +16 -1
  41. data/lib/grape/middleware/auth/strategy_info.rb +2 -2
  42. data/lib/grape/middleware/base.rb +19 -13
  43. data/lib/grape/middleware/error.rb +133 -35
  44. data/lib/grape/middleware/formatter.rb +235 -94
  45. data/lib/grape/middleware/precomputed_content_types.rb +14 -2
  46. data/lib/grape/middleware/stack.rb +7 -8
  47. data/lib/grape/middleware/versioner/base.rb +26 -10
  48. data/lib/grape/middleware/versioner/header.rb +70 -15
  49. data/lib/grape/middleware/versioner/param.rb +3 -5
  50. data/lib/grape/middleware/versioner/path.rb +20 -11
  51. data/lib/grape/middleware/versioner.rb +1 -1
  52. data/lib/grape/mountable.rb +2 -4
  53. data/lib/grape/namespace.rb +2 -3
  54. data/lib/grape/parser/jsonl.rb +19 -0
  55. data/lib/grape/request.rb +46 -8
  56. data/lib/grape/router/base_route.rb +41 -0
  57. data/lib/grape/router/greedy_route.rb +1 -1
  58. data/lib/grape/router/mustermann_pattern.rb +1 -1
  59. data/lib/grape/router/pattern/path.rb +1 -1
  60. data/lib/grape/router/pattern.rb +11 -5
  61. data/lib/grape/router/route.rb +42 -11
  62. data/lib/grape/router/route_buckets.rb +246 -0
  63. data/lib/grape/router.rb +262 -32
  64. data/lib/grape/util/endpoint_configuration.rb +7 -1
  65. data/lib/grape/util/inheritable_setting.rb +80 -161
  66. data/lib/grape/util/lazy/value.rb +52 -11
  67. data/lib/grape/util/media_type.rb +38 -15
  68. data/lib/grape/util/path_normalizer.rb +7 -2
  69. data/lib/grape/util/registry.rb +9 -2
  70. data/lib/grape/util/scrub.rb +33 -0
  71. data/lib/grape/util/translation.rb +6 -6
  72. data/lib/grape/validations/attributes_iterator.rb +73 -37
  73. data/lib/grape/validations/coerce_options.rb +1 -1
  74. data/lib/grape/validations/params_documentation.rb +15 -7
  75. data/lib/grape/validations/params_scope.rb +176 -43
  76. data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
  77. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  78. data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
  79. data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
  80. data/lib/grape/validations/types/primitive_coercer.rb +17 -0
  81. data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
  82. data/lib/grape/validations/types.rb +12 -18
  83. data/lib/grape/validations/validations_spec.rb +53 -22
  84. data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
  85. data/lib/grape/validations/validators/as_validator.rb +2 -3
  86. data/lib/grape/validations/validators/base.rb +124 -25
  87. data/lib/grape/validations/validators/coerce_validator.rb +2 -3
  88. data/lib/grape/validations/validators/default_validator.rb +18 -2
  89. data/lib/grape/validations/validators/except_values_validator.rb +16 -3
  90. data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
  91. data/lib/grape/validations/validators/length_validator.rb +24 -16
  92. data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
  93. data/lib/grape/validations/validators/values_validator.rb +18 -24
  94. data/lib/grape/version.rb +1 -1
  95. data/lib/grape.rb +3 -0
  96. metadata +18 -11
  97. data/lib/grape/util/lazy/value_array.rb +0 -21
  98. data/lib/grape/util/lazy/value_enumerable.rb +0 -31
  99. data/lib/grape/util/lazy/value_hash.rb +0 -21
  100. data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
  101. data/lib/grape/validations/single_attribute_iterator.rb +0 -28
@@ -3,9 +3,9 @@
3
3
  module Grape
4
4
  module DSL
5
5
  # Immutable value object holding the resolved options from
6
- # +Grape::DSL::Routing#version+. Stored on the inheritable settings as
6
+ # +Grape::DSL::Versioning#version+. Stored on the inheritable settings as
7
7
  # +Grape::Util::InheritableSetting#version_options+ and read by internal call
8
- # sites (`Path`, `Endpoint`, `API::Instance#cascade?`,
8
+ # sites (`Path`, `Endpoint`, `API::Instance`,
9
9
  # `Middleware::Versioner::Base`) via accessors.
10
10
  #
11
11
  # Defaults are duplicated on +#initialize+ here and on +#version+'s
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Versioning
6
+ # Called without a value, answers whether the API cascades: the value
7
+ # given here, else the +cascade:+ of +version+, else true. The compiled
8
+ # API reads it here too; this reader used to skip +version+, and so
9
+ # answered true for an API that did not cascade.
10
+ def cascade(value = nil)
11
+ return inheritable_setting.cascade = value unless value.nil?
12
+
13
+ setting = inheritable_setting
14
+ return setting.cascade if setting.cascade_defined?
15
+ return setting.version_options.cascade if setting.version_options
16
+
17
+ true
18
+ end
19
+
20
+ # Specify an API version.
21
+ #
22
+ # Called without arguments, returns the most recently declared version
23
+ # (or +nil+). Called with one or more version strings, registers them
24
+ # and stores a {Grape::DSL::VersionOptions} value object on the
25
+ # inheritable settings; when given a block, the registration applies
26
+ # within a nested namespace.
27
+ #
28
+ # @example API with legacy support.
29
+ # class MyAPI < Grape::API
30
+ # version 'v2'
31
+ #
32
+ # get '/main' do
33
+ # {some: 'data'}
34
+ # end
35
+ #
36
+ # version 'v1' do
37
+ # get '/main' do
38
+ # {legacy: 'data'}
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # @param args [Array<String, Symbol>] one or more version identifiers.
44
+ # @param using [Symbol] versioning strategy — one of +:path+ (default),
45
+ # +:header+, +:param+, or +:accept_version_header+.
46
+ # @param cascade [Boolean] forward to subsequent routes via the
47
+ # +X-Cascade+ header on version mismatch. Defaults to +true+.
48
+ # @param parameter [String] name of the query/body parameter that
49
+ # carries the version when +using: :param+. Defaults to +'apiver'+.
50
+ # @param strict [Boolean] reject requests that don't supply a usable
51
+ # version (header strategies). Defaults to +false+.
52
+ # @param vendor [String, nil] vendor segment for the +:header+
53
+ # strategy (+application/vnd.<vendor>-<version>+); required when
54
+ # +using: :header+.
55
+ # @yield optional block to scope routes under this version.
56
+ # @return [String, nil] the most recently declared version.
57
+ # @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+
58
+ # is supplied without a +:vendor+.
59
+ def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
60
+ return @versions&.last if args.empty?
61
+
62
+ options = build_version_options(using:, cascade:, parameter:, strict:, vendor:)
63
+ requested_versions = args.flatten.map(&:to_s)
64
+
65
+ @versions = versions | requested_versions
66
+
67
+ if block
68
+ within_namespace do
69
+ apply_version(requested_versions, options)
70
+ instance_eval(&block)
71
+ end
72
+ else
73
+ apply_version(requested_versions, options)
74
+ end
75
+
76
+ @versions.last
77
+ end
78
+
79
+ # @return array of defined versions
80
+ def versions
81
+ @versions ||= []
82
+ end
83
+
84
+ private
85
+
86
+ def build_version_options(using:, vendor:, **)
87
+ raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil?
88
+
89
+ VersionOptions.new(using:, vendor:, **)
90
+ end
91
+
92
+ def apply_version(versions, options)
93
+ inheritable_setting.version = versions
94
+ inheritable_setting.version_options = options
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Endpoint
5
+ # Everything an endpoint's requests read from it and none of them changes.
6
+ # Each request's copy of the endpoint holds this and its own state, and
7
+ # nothing else of the endpoint. +app+, +callbacks+ and +build_params_with+
8
+ # are filled in by Endpoint#compile!.
9
+ State = Data.define(:inheritable_setting, :options, :config, :source, :options_route_enabled, :app, :callbacks, :build_params_with) do
10
+ def initialize(inheritable_setting:, options:, config:, source:, options_route_enabled: false, app: nil, callbacks: nil, build_params_with: nil)
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -11,14 +11,41 @@ module Grape
11
11
  include Grape::DSL::Headers
12
12
  include Grape::DSL::InsideRoute
13
13
 
14
- attr_reader :env, :request, :source, :options, :endpoints
15
- attr_accessor :options_route_enabled
14
+ # The innermost app of every endpoint's Rack stack. The endpoint to run
15
+ # comes out of the env, so the lambda holds no state and one of them serves
16
+ # every stack, rather than one being allocated per endpoint.
17
+ RUN_ENDPOINT = ->(env) { env[Grape::Env::API_ENDPOINT].run }
18
+ private_constant :RUN_ENDPOINT
19
+
20
+ attr_reader :env, :request, :endpoints
21
+
22
+ # Written out rather than delegated: the request path reads them, and a
23
+ # +def_delegator+ method checks +defined?+ on every call.
24
+ def inheritable_setting
25
+ @state.inheritable_setting
26
+ end
27
+
28
+ def options
29
+ @state.options
30
+ end
31
+
32
+ def source
33
+ @state.source
34
+ end
35
+
36
+ def options_route_enabled
37
+ @state.options_route_enabled
38
+ end
39
+
40
+ def options_route_enabled=(enabled)
41
+ @state = @state.with(options_route_enabled: enabled)
42
+ end
16
43
 
17
44
  def_delegators :request, :params, :headers, :cookies
18
45
  def_delegator :cookies, :response_cookies
19
46
 
20
47
  # The API (a +Grape::API+ instance) this endpoint belongs to.
21
- def_delegator :@config, :api
48
+ def_delegator :config, :api
22
49
 
23
50
  # The logger configured on the API this endpoint belongs to. Available
24
51
  # inside route handlers, +before+/+after+/+after_validation+/+finally+
@@ -32,6 +59,13 @@ module Grape
32
59
  config.app
33
60
  end
34
61
 
62
+ # The paths this endpoint was declared at, relative to its {#namespace},
63
+ # as an Array: the paths given to +route+, or for a mount the path
64
+ # {#mounted_app} was mounted at.
65
+ def path
66
+ config.path
67
+ end
68
+
35
69
  class << self
36
70
  def block_to_unbound_method(block)
37
71
  return unless block
@@ -67,21 +101,18 @@ module Grape
67
101
  # endpoint does not know if it will be mounted under a different endpoint.
68
102
  # @yield a block defining what your API should do when this endpoint is hit
69
103
  def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
70
- self.inheritable_setting = new_settings.point_in_time_copy_for_endpoint
71
-
72
- @options = options
73
- @config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
104
+ config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
74
105
  # +:app+ is still surfaced on the public options Hash for backwards
75
106
  # compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
76
- @options[:app] = app if app
107
+ options[:app] = app if app
77
108
 
78
- @status = nil
79
- @stream = nil
80
- @body = nil
81
- @source = self.class.block_to_unbound_method(block)
82
- @before_filter_passed = false
83
- @options_route_enabled = false
84
- @endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints)
109
+ @state = State.new(
110
+ inheritable_setting: new_settings.point_in_time_copy_for_endpoint,
111
+ options:,
112
+ config:,
113
+ source: self.class.block_to_unbound_method(block)
114
+ )
115
+ @endpoints = app.endpoints if app.respond_to?(:endpoints)
85
116
  end
86
117
 
87
118
  # Update our settings from a given parent settings instance. Used when
@@ -110,12 +141,7 @@ module Grape
110
141
 
111
142
  reset_routes!
112
143
  compile!
113
- routes.each do |route|
114
- router.append(route.apply(self))
115
- next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
116
-
117
- router.append(route.to_head.apply(self))
118
- end
144
+ append_routes_to(router)
119
145
  end
120
146
 
121
147
  def namespace
@@ -129,7 +155,7 @@ module Grape
129
155
  def call!(env)
130
156
  env[Grape::Env::API_ENDPOINT] = self
131
157
  @env = env
132
- @app.call(env)
158
+ @state.app.call(env)
133
159
  end
134
160
 
135
161
  def ==(other)
@@ -156,45 +182,32 @@ module Grape
156
182
  "#{self.class} in '#{route.origin}' endpoint"
157
183
  end
158
184
 
159
- protected
160
-
185
+ # Serve the request the endpoint's Rack stack was entered with: its
186
+ # +before+ filters, its validations, the route block, its +after+ and
187
+ # +finally+ filters, answered as a Rack triplet. The innermost app of that
188
+ # stack calls it, so it expects #call! to have put the env in place first.
161
189
  def run
162
190
  instrument_run do
163
- @request = Grape::Request.new(env, build_params_with: @build_params_with)
191
+ @request = Grape::Request.new(env, build_params_with: @state.build_params_with)
192
+ callbacks = @state.callbacks
164
193
  begin
165
- run_filters befores, :before
194
+ run_filters callbacks, :before if callbacks
166
195
  @before_filter_passed = true
167
196
 
168
- if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS)
169
- header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
170
- raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
171
-
172
- response_object = ''
173
- status 204
174
- else
175
- run_filters before_validations, :before_validation
176
- run_validators(request:)
177
- run_filters after_validations, :after_validation
178
- response_object = execute
179
- end
180
-
181
- run_filters afters, :after
197
+ response_object = env.key?(Grape::Env::GRAPE_ALLOWED_METHODS) ? answer_allowed_methods : validate_and_execute(callbacks)
198
+ run_filters callbacks, :after if callbacks
182
199
  build_response_cookies
183
-
184
- # status verifies body presence when DELETE
185
- @body ||= response_object
186
-
187
- # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
188
- response_object = stream || [body]
189
-
190
- [status, header, response_object]
200
+ rack_triplet(response_object)
191
201
  ensure
192
- run_filters finallies, :finally
202
+ run_filters callbacks, :finally if callbacks
193
203
  end
194
204
  end
195
205
  end
196
206
 
207
+ protected
208
+
197
209
  def execute
210
+ source = @state.source
198
211
  return unless source
199
212
 
200
213
  instrument_render do
@@ -203,8 +216,8 @@ module Grape
203
216
  end
204
217
 
205
218
  def run_validators(request:)
206
- validators = inheritable_setting.route_validations
207
- return if validators.blank?
219
+ validators = @state.inheritable_setting.route_validations
220
+ return if validators.empty?
208
221
 
209
222
  validation_exceptions = nil
210
223
 
@@ -222,24 +235,73 @@ module Grape
222
235
  raise Grape::Exceptions::ValidationErrors.new(exceptions: validation_exceptions, headers: header) if validation_exceptions
223
236
  end
224
237
 
225
- def run_filters(filters, type = :other)
226
- return if filters.blank?
238
+ # An endpoint with filters still has none in most of its five phases, so
239
+ # an empty phase returns before it is instrumented.
240
+ def run_filters(callbacks, type)
241
+ filters = callbacks[type]
242
+ return if filters.empty?
227
243
 
228
244
  instrument_run_filters(filters, type) do
229
245
  filters.each { |filter| instance_eval(&filter) }
230
246
  end
231
247
  end
232
248
 
233
- attr_reader :befores, :before_validations, :after_validations, :afters, :finallies, :config
249
+ def config
250
+ @state.config
251
+ end
234
252
 
235
253
  def options?
236
254
  options_route_enabled && env[Rack::REQUEST_METHOD] == Rack::OPTIONS
237
255
  end
238
256
 
257
+ # Makes an +allocate+d endpoint the prototype #build_prototype answers:
258
+ # +state+, and the ivars a request sets, in place so that every copy
259
+ # starts with the same shape.
260
+ def become_prototype(state)
261
+ @state = state
262
+ @status = nil
263
+ @stream = nil
264
+ @body = nil
265
+ @before_filter_passed = false
266
+ self
267
+ end
268
+
239
269
  private
240
270
 
241
271
  attr_reader :before_filter_passed
242
272
 
273
+ # The answer of a greedy route, which the router hands a request for a
274
+ # method its path has no route for: the Allow header, with a 204 to an
275
+ # auto-OPTIONS and a 405 to anything else.
276
+ def answer_allowed_methods
277
+ header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
278
+ raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
279
+
280
+ status 204
281
+ ''
282
+ end
283
+
284
+ # The route block's value, run once the params are validated between the
285
+ # +before_validation+ and +after_validation+ filters.
286
+ def validate_and_execute(callbacks)
287
+ run_filters callbacks, :before_validation if callbacks
288
+ run_validators(request:)
289
+ run_filters callbacks, :after_validation if callbacks
290
+ execute
291
+ end
292
+
293
+ # The response #run answers with, its body +route_value+ unless the
294
+ # endpoint set a body or a stream itself.
295
+ def rack_triplet(route_value)
296
+ # status verifies body presence when DELETE
297
+ @body ||= route_value
298
+
299
+ # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
300
+ response_body = stream || [body]
301
+
302
+ [status, header, response_body]
303
+ end
304
+
243
305
  # Instrument helpers. Each guards on +listening?+ so that with no subscriber
244
306
  # the payload Hash and notification machinery are skipped and the block runs
245
307
  # directly (no added allocations); the block is forwarded anonymously so
@@ -269,45 +331,49 @@ module Grape
269
331
  end
270
332
 
271
333
  def compile!
272
- @app = config.app || build_stack
334
+ app = config.app || build_stack
273
335
  warn_unauthenticated_mounted_app
274
- @helpers = build_helpers
336
+ # nil when no phase has a filter -- most endpoints -- so that #run skips
337
+ # all five on one read.
275
338
  callbacks = inheritable_setting.callbacks
276
- @befores = callbacks.fetch(:before)
277
- @before_validations = callbacks.fetch(:before_validation)
278
- @after_validations = callbacks.fetch(:after_validation)
279
- @afters = callbacks.fetch(:after)
280
- @finallies = callbacks.fetch(:finally)
281
- @build_params_with = inheritable_setting.build_params_with
282
- @prototype = build_prototype
339
+ @state = @state.with(
340
+ app:,
341
+ callbacks: callbacks.each_value.all?(&:empty?) ? nil : callbacks.freeze,
342
+ build_params_with: inheritable_setting.build_params_with
343
+ )
344
+ @prototype = build_prototype(build_helpers)
283
345
  end
284
346
 
285
- # The object each request is copied from.
347
+ # The object each request is copied from: this endpoint's State, and none
348
+ # of the route-building state (+@prototype+, +@endpoints+, +@namespace+,
349
+ # +@routes+) the request path never reads.
350
+ #
351
+ # Ruby sizes a new object by the most instance variables any object of its
352
+ # class has held. A copy holds the State and the seven ivars a request
353
+ # sets, which fits an 80-byte slot until a route block or a helper sets one
354
+ # of its own; carrying the route-building state as well, it took 320. The
355
+ # endpoint itself counts too when there are no helpers, both being a
356
+ # Grape::Endpoint, and it holds at most five.
286
357
  #
287
358
  # Helpers reach an endpoint by module inclusion, and +Object#dup+ does not
288
359
  # carry a singleton class over — so including them on the per-request copy
289
360
  # meant giving every request a brand-new singleton class, whose method
290
361
  # cache then started cold for the helpers, the route block and every DSL
291
362
  # method the endpoint answers. Include them once into a subclass owned by
292
- # this endpoint instead, and copy this endpoint's state onto an instance of
293
- # it: the per-request copy already answers to the helpers, against a class
294
- # that has been warm since boot. It also keeps request handling free of
295
- # class mutation.
296
- #
297
- # Endpoints with no helpers are their own prototype, exactly as before.
363
+ # this endpoint instead, and hand its State to an instance of it: the
364
+ # per-request copy already answers to the helpers, against a class that
365
+ # has been warm since boot. It also keeps request handling free of class
366
+ # mutation.
298
367
  #
299
- # +allocate+ rather than +new+ because the constructor rebuilds state from
300
- # the DSL's arguments — a fresh settings copy, a fresh +@config+, another
301
- # +block_to_unbound_method+ — and would still not hold what +compile!+ has
302
- # just computed. What is wanted is what +dup+ itself does (allocate, then
303
- # copy the ivars over) across a class boundary, which neither +dup+ nor
304
- # +initialize_copy+ will do: both insist on the receiver's own class. So
305
- # the copy is written out here. Taking all of them is safe because
306
- # everything set past this point (+routes+, +namespace+) is route-building
307
- # memoization the request path never reads.
308
- def build_prototype
309
- return self unless @helpers
368
+ # +allocate+ rather than +new+ because the constructor builds an endpoint
369
+ # from the DSL's arguments — a fresh settings copy, a fresh +config+,
370
+ # another +block_to_unbound_method+ — and would still not hold what
371
+ # +compile!+ has just computed, while the State already holds all of it.
372
+ def build_prototype(helpers)
373
+ (helpers ? helpers_class(helpers) : self.class).allocate.become_prototype(@state)
374
+ end
310
375
 
376
+ def helpers_class(helpers)
311
377
  klass = Class.new(self.class)
312
378
  # Anonymous classes answer nil to +name+ and render as a bare address,
313
379
  # which would reach anything that reports on the endpoint — +inspect+
@@ -315,41 +381,59 @@ module Grape
315
381
  # +set_temporary_name+ is for; a plain constant path is rejected, which
316
382
  # is right, since this class is not reachable under that name.
317
383
  klass.set_temporary_name("#{self.class}(helpers)")
318
- klass.include(@helpers)
319
- prototype = klass.allocate
320
- # Everything but the prototype slot itself, so that recompiling (a
321
- # remount) does not leave the new prototype holding the one it replaces.
322
- (instance_variables - [:@prototype]).each { |name| prototype.instance_variable_set(name, instance_variable_get(name)) }
323
- prototype
384
+ klass.include(helpers)
385
+ end
386
+
387
+ # Each route, and after a GET route the HEAD route it also answers unless
388
+ # the API declared +do_not_route_head!+.
389
+ def append_routes_to(router)
390
+ routes.each do |route|
391
+ router.append(route.apply(self))
392
+ next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
393
+
394
+ router.append(route.to_head.apply(self))
395
+ end
324
396
  end
325
397
 
326
398
  def to_routes
399
+ patterns = route_patterns
327
400
  route_options = config.route_options
328
- params = config.params
401
+ attributes = route_attributes
402
+
403
+ config.http_methods.flat_map do |method|
404
+ patterns.map { |pattern| Grape::Router::Route.new(self, method, pattern, route_options, **attributes) }
405
+ end
406
+ end
407
+
408
+ # The pattern of each of the endpoint's paths, shared by the routes of
409
+ # every method it answers, as a HEAD route shares its GET route's.
410
+ def route_patterns
329
411
  path_settings = inheritable_setting.path_settings
330
- forward_match = bare_rack_app?
331
- version = prepare_version(inheritable_setting.version)
332
- prefix = inheritable_setting.root_prefix
333
- requirements = prepare_routes_requirements(config.requirements)
412
+ params = config.params
413
+ version = prepare_version
414
+ requirements = prepare_routes_requirements
334
415
  anchor = config.anchor
335
- settings = inheritable_setting.route_settings
336
416
 
337
- config.http_methods.flat_map do |method|
338
- config.path.map do |path|
339
- pattern = Grape::Router::Pattern.build(
340
- path:,
341
- namespace:,
342
- settings: path_settings,
343
- anchor:,
344
- params:,
345
- version:,
346
- requirements:
347
- )
348
- Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, params:, namespace:, prefix:, settings:)
349
- end
417
+ config.path.map do |path|
418
+ Grape::Router::Pattern.build(
419
+ path:,
420
+ namespace:,
421
+ settings: path_settings,
422
+ anchor:,
423
+ params:,
424
+ version:,
425
+ requirements:
426
+ )
350
427
  end
351
428
  end
352
429
 
430
+ # What every route of the endpoint is built with besides its method,
431
+ # pattern and options, read once for all of them.
432
+ def route_attributes
433
+ setting = inheritable_setting
434
+ { forward_match: bare_rack_app?, params: config.params, namespace:, prefix: setting.root_prefix, settings: setting.route_settings }
435
+ end
436
+
353
437
  # True when a bare Rack app (anything that isn't a Grape app) is mounted at
354
438
  # this endpoint. Such an app is called directly and matched by path prefix
355
439
  # rather than an anchored route.
@@ -357,14 +441,16 @@ module Grape
357
441
  config.app && !config.app.is_a?(Grape::Mountable)
358
442
  end
359
443
 
360
- def prepare_routes_requirements(route_options_requirements)
444
+ def prepare_routes_requirements
361
445
  namespace_requirements = inheritable_setting.namespace_requirements
362
- namespace_requirements << route_options_requirements if route_options_requirements.present?
363
- namespace_requirements.reduce({}, :merge)
446
+ route_requirements = config.requirements
447
+ namespace_requirements << route_requirements if route_requirements
448
+ {}.merge!(*namespace_requirements)
364
449
  end
365
450
 
366
- def prepare_version(namespace_inheritable_version)
367
- return if namespace_inheritable_version.blank?
451
+ def prepare_version
452
+ namespace_inheritable_version = inheritable_setting.version
453
+ return if namespace_inheritable_version.nil? || namespace_inheritable_version.empty?
368
454
 
369
455
  namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
370
456
  end
@@ -372,36 +458,46 @@ module Grape
372
458
  def build_stack
373
459
  stack = Grape::Middleware::Stack.new
374
460
 
375
- content_types = inheritable_setting.content_types
376
- format = inheritable_setting.format
461
+ setting = inheritable_setting
462
+ content_types = setting.content_types
463
+ format = setting.format
377
464
 
378
- stack.use Rack::Head
379
- stack.use Rack::Lint if lint?
380
465
  stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
381
466
 
382
- stack.concat inheritable_setting.middleware
383
-
384
- if inheritable_setting.version.present?
385
- version_options = inheritable_setting.version_options
386
- stack.use Grape::Middleware::Versioner.using(version_options.using),
387
- versions: inheritable_setting.version.flatten,
388
- version_options:,
389
- prefix: inheritable_setting.root_prefix,
390
- mount_path: inheritable_setting.mount_path
391
- end
392
-
393
- stack.use Grape::Middleware::Formatter,
394
- format:,
395
- default_format: inheritable_setting.default_format || :txt,
396
- content_types:,
397
- formatters: inheritable_setting.formatters,
398
- parsers: inheritable_setting.parsers
467
+ stack.concat setting.middleware
468
+ use_versioner(stack)
469
+ stack.use Grape::Middleware::Formatter, **formatter_middleware_options(format, content_types)
399
470
 
400
471
  builder = stack.build
401
- builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
472
+ builder.run RUN_ENDPOINT
402
473
  builder.to_app
403
474
  end
404
475
 
476
+ # The versioner for the strategy +version+ declared, when it declared one.
477
+ def use_versioner(stack)
478
+ setting = inheritable_setting
479
+ versions = setting.version
480
+ return if versions.nil? || versions.empty?
481
+
482
+ version_options = setting.version_options
483
+ stack.use Grape::Middleware::Versioner.using(version_options.using),
484
+ versions: versions.flatten,
485
+ version_options:,
486
+ prefix: setting.root_prefix,
487
+ mount_path: setting.mount_path
488
+ end
489
+
490
+ def formatter_middleware_options(format, content_types)
491
+ setting = inheritable_setting
492
+ {
493
+ format:,
494
+ default_format: setting.default_format || :txt,
495
+ content_types:,
496
+ formatters: setting.formatters,
497
+ parsers: setting.parsers
498
+ }
499
+ end
500
+
405
501
  def error_middleware_options(format, content_types)
406
502
  setting = inheritable_setting
407
503
  {
@@ -449,9 +545,5 @@ module Grape
449
545
  Rack::Utils.set_cookie_header! header, name, cookie_value
450
546
  end
451
547
  end
452
-
453
- def lint?
454
- inheritable_setting.lint? || Grape.config.lint
455
- end
456
548
  end
457
549
  end