grape 3.0.1 → 3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CONTRIBUTING.md +2 -2
  4. data/README.md +2 -2
  5. data/UPGRADING.md +14 -0
  6. data/grape.gemspec +2 -2
  7. data/lib/grape/api/instance.rb +16 -47
  8. data/lib/grape/api.rb +6 -10
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dsl/declared.rb +35 -0
  12. data/lib/grape/dsl/helpers.rb +2 -2
  13. data/lib/grape/dsl/inside_route.rb +3 -141
  14. data/lib/grape/dsl/parameters.rb +8 -26
  15. data/lib/grape/dsl/routing.rb +41 -29
  16. data/lib/grape/dsl/settings.rb +1 -1
  17. data/lib/grape/dsl/validations.rb +22 -20
  18. data/lib/grape/endpoint.rb +84 -83
  19. data/lib/grape/exceptions/base.rb +1 -1
  20. data/lib/grape/middleware/auth/dsl.rb +4 -4
  21. data/lib/grape/middleware/base.rb +4 -0
  22. data/lib/grape/middleware/error.rb +1 -1
  23. data/lib/grape/middleware/formatter.rb +6 -4
  24. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  25. data/lib/grape/middleware/versioner/base.rb +20 -0
  26. data/lib/grape/middleware/versioner/header.rb +1 -17
  27. data/lib/grape/middleware/versioner/path.rb +1 -1
  28. data/lib/grape/namespace.rb +5 -9
  29. data/lib/grape/params_builder.rb +2 -19
  30. data/lib/grape/router/base_route.rb +14 -5
  31. data/lib/grape/router/greedy_route.rb +11 -5
  32. data/lib/grape/router/pattern.rb +6 -20
  33. data/lib/grape/router/route.rb +7 -11
  34. data/lib/grape/router.rb +35 -61
  35. data/lib/grape/util/api_description.rb +10 -8
  36. data/lib/grape/validations/attributes_iterator.rb +2 -2
  37. data/lib/grape/validations/params_scope.rb +12 -10
  38. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  39. data/lib/grape/validations/validators/base.rb +2 -2
  40. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  41. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +1 -1
  42. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  43. data/lib/grape/validations/validators/regexp_validator.rb +12 -2
  44. data/lib/grape/validations/validators/values_validator.rb +1 -1
  45. data/lib/grape/version.rb +1 -1
  46. data/lib/grape.rb +5 -13
  47. metadata +11 -14
  48. data/lib/grape/exceptions/missing_option.rb +0 -11
  49. data/lib/grape/exceptions/unknown_options.rb +0 -11
  50. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  51. data/lib/grape/extensions/hash.rb +0 -27
  52. data/lib/grape/extensions/hashie/mash.rb +0 -24
@@ -160,33 +160,15 @@ module Grape
160
160
  # Define common settings for one or more parameters
161
161
  # @param (see #requires)
162
162
  # @option (see #requires)
163
- def with(*attrs, &block)
163
+ def with(*attrs, &)
164
164
  new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge)
165
- new_group_scope([new_group_attrs], &block)
165
+ new_group_scope([new_group_attrs], &)
166
166
  end
167
167
 
168
- # Disallow the given parameters to be present in the same request.
169
- # @param attrs [*Symbol] parameters to validate
170
- def mutually_exclusive(*attrs)
171
- validates(attrs, mutual_exclusion: { value: true, message: extract_message_option(attrs) })
172
- end
173
-
174
- # Require exactly one of the given parameters to be present.
175
- # @param (see #mutually_exclusive)
176
- def exactly_one_of(*attrs)
177
- validates(attrs, exactly_one_of: { value: true, message: extract_message_option(attrs) })
178
- end
179
-
180
- # Require at least one of the given parameters to be present.
181
- # @param (see #mutually_exclusive)
182
- def at_least_one_of(*attrs)
183
- validates(attrs, at_least_one_of: { value: true, message: extract_message_option(attrs) })
184
- end
185
-
186
- # Require that either all given params are present, or none are.
187
- # @param (see #mutually_exclusive)
188
- def all_or_none_of(*attrs)
189
- validates(attrs, all_or_none_of: { value: true, message: extract_message_option(attrs) })
168
+ %i[mutually_exclusive exactly_one_of at_least_one_of all_or_none_of].each do |validator|
169
+ define_method validator do |*attrs, message: nil|
170
+ validates(attrs, validator => { value: true, message: message })
171
+ end
190
172
  end
191
173
 
192
174
  # Define a block of validations which should be applied if and only if
@@ -196,12 +178,12 @@ module Grape
196
178
  # @raise Grape::Exceptions::UnknownParameter if `attr` has not been
197
179
  # defined in this scope yet
198
180
  # @yield a parameter definition DSL
199
- def given(*attrs, &block)
181
+ def given(*attrs, &)
200
182
  attrs.each do |attr|
201
183
  proxy_attr = first_hash_key_or_param(attr)
202
184
  raise Grape::Exceptions::UnknownParameter.new(proxy_attr) unless declared_param?(proxy_attr)
203
185
  end
204
- new_lateral_scope(dependent_on: attrs, &block)
186
+ new_lateral_scope(dependent_on: attrs, &)
205
187
  end
206
188
 
207
189
  # Test for whether a certain parameter has been defined in this params
@@ -5,6 +5,22 @@ module Grape
5
5
  module Routing
6
6
  attr_reader :endpoints
7
7
 
8
+ def given(conditional_option, &)
9
+ return unless conditional_option
10
+
11
+ mounted(&)
12
+ end
13
+
14
+ def mounted(&block)
15
+ evaluate_as_instance_with_configuration(block, lazy: true)
16
+ end
17
+
18
+ def cascade(value = nil)
19
+ return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable[:cascade].nil? : true if value.nil?
20
+
21
+ inheritable_setting.namespace_inheritable[:cascade] = value
22
+ end
23
+
8
24
  # Specify an API version.
9
25
  #
10
26
  # @example API with legacy support.
@@ -141,7 +157,7 @@ module Grape
141
157
  # {hello: 'world'}
142
158
  # end
143
159
  # end
144
- def route(methods, paths = ['/'], route_options = {}, &block)
160
+ def route(methods, paths = ['/'], route_options = {}, &)
145
161
  method = methods == :any ? '*' : methods
146
162
  endpoint_params = inheritable_setting.namespace_stackable_with_hash(:params) || {}
147
163
  endpoint_description = inheritable_setting.route[:description]
@@ -149,14 +165,14 @@ module Grape
149
165
  all_route_options.deep_merge!(endpoint_description) if endpoint_description
150
166
  all_route_options.deep_merge!(route_options) if route_options&.any?
151
167
 
152
- endpoint_options = {
168
+ new_endpoint = Grape::Endpoint.new(
169
+ inheritable_setting,
153
170
  method: method,
154
171
  path: paths,
155
172
  for: self,
156
- route_options: all_route_options
157
- }
158
-
159
- new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block)
173
+ route_options: all_route_options,
174
+ &
175
+ )
160
176
  endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) }
161
177
 
162
178
  inheritable_setting.route_end
@@ -182,12 +198,12 @@ module Grape
182
198
  # # defines the endpoint: GET /foo/bar
183
199
  # end
184
200
  # end
185
- def namespace(space = nil, options = {}, &block)
201
+ def namespace(space = nil, requirements: nil, **options, &block)
186
202
  return Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) unless space || block
187
203
 
188
204
  within_namespace do
189
205
  nest(block) do
190
- inheritable_setting.namespace_stackable[:namespace] = Grape::Namespace.new(space, options) if space
206
+ inheritable_setting.namespace_stackable[:namespace] = Grape::Namespace.new(space, requirements: requirements, **options) if space
191
207
  end
192
208
  end
193
209
  end
@@ -202,33 +218,19 @@ module Grape
202
218
  @routes ||= endpoints.map(&:routes).flatten
203
219
  end
204
220
 
205
- # Remove all defined routes.
206
- def reset_routes!
207
- endpoints.each(&:reset_routes!)
208
- @routes = nil
209
- end
210
-
211
- def reset_endpoints!
212
- @endpoints = []
213
- end
214
-
215
221
  # This method allows you to quickly define a parameter route segment
216
222
  # in your API.
217
223
  #
218
224
  # @param param [Symbol] The name of the parameter you wish to declare.
219
225
  # @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
220
- def route_param(param, options = {}, &block)
221
- options = options.dup
222
-
223
- options[:requirements] = {
224
- param.to_sym => options[:requirements]
225
- } if options[:requirements].is_a?(Regexp)
226
+ def route_param(param, requirements: nil, type: nil, **options, &)
227
+ requirements = { param.to_sym => requirements } if requirements.is_a?(Regexp)
226
228
 
227
229
  Grape::Validations::ParamsScope.new(api: self) do
228
- requires param, type: options[:type]
229
- end if options.key?(:type)
230
+ requires param, type: type
231
+ end if type
230
232
 
231
- namespace(":#{param}", options, &block)
233
+ namespace(":#{param}", requirements: requirements, **options, &)
232
234
  end
233
235
 
234
236
  # @return array of defined versions
@@ -238,6 +240,16 @@ module Grape
238
240
 
239
241
  private
240
242
 
243
+ # Remove all defined routes.
244
+ def reset_routes!
245
+ endpoints.each(&:reset_routes!)
246
+ @routes = nil
247
+ end
248
+
249
+ def reset_endpoints!
250
+ @endpoints = []
251
+ end
252
+
241
253
  def refresh_mounted_api(mounts, *opts)
242
254
  opts << { refresh_already_mounted: true }
243
255
  mount(mounts, *opts)
@@ -260,12 +272,12 @@ module Grape
260
272
  def evaluate_as_instance_with_configuration(block, lazy: false)
261
273
  lazy_block = Grape::Util::Lazy::Block.new do |configuration|
262
274
  value_for_configuration = configuration
263
- self.configuration = value_for_configuration.evaluate if value_for_configuration.try(:lazy?)
275
+ self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy?
264
276
  response = instance_eval(&block)
265
277
  self.configuration = value_for_configuration
266
278
  response
267
279
  end
268
- if base && base_instance? && lazy
280
+ if @base && base_instance? && lazy
269
281
  lazy_block
270
282
  else
271
283
  lazy_block.evaluate_from(configuration)
@@ -7,7 +7,7 @@ module Grape
7
7
  # matter where they're defined, and inheritable settings which apply only
8
8
  # in the current scope and scopes nested under it.
9
9
  module Settings
10
- attr_writer :inheritable_setting, :top_level_setting
10
+ attr_writer :inheritable_setting
11
11
 
12
12
  # Fetch our top-level settings, which apply to all endpoints in the API.
13
13
  def top_level_setting
@@ -3,6 +3,28 @@
3
3
  module Grape
4
4
  module DSL
5
5
  module Validations
6
+ # Opens a root-level ParamsScope, defining parameter coercions and
7
+ # validations for the endpoint.
8
+ # @yield instance context of the new scope
9
+ def params(&)
10
+ Grape::Validations::ParamsScope.new(api: self, type: Hash, &)
11
+ end
12
+
13
+ # Declare the contract to be used for the endpoint's parameters.
14
+ # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
15
+ # The contract or schema to be used for validation. Optional.
16
+ # @yield a block yielding a new instance of Dry::Schema::Params
17
+ # subclass, allowing to define the schema inline. When the
18
+ # +contract+ parameter is a schema, it will be used as a parent. Optional.
19
+ def contract(contract = nil, &block)
20
+ raise ArgumentError, 'Either contract or block must be provided' unless contract || block
21
+ raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
22
+
23
+ Grape::Validations::ContractScope.new(self, contract, &block)
24
+ end
25
+
26
+ private
27
+
6
28
  # Clears all defined parameters and validations. The main purpose of it is to clean up
7
29
  # settings, so next endpoint won't interfere with previous one.
8
30
  #
@@ -24,26 +46,6 @@ module Grape
24
46
  def reset_validations!
25
47
  inheritable_setting.namespace_stackable.delete(:declared_params, :params, :validations)
26
48
  end
27
-
28
- # Opens a root-level ParamsScope, defining parameter coercions and
29
- # validations for the endpoint.
30
- # @yield instance context of the new scope
31
- def params(&block)
32
- Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
33
- end
34
-
35
- # Declare the contract to be used for the endpoint's parameters.
36
- # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
37
- # The contract or schema to be used for validation. Optional.
38
- # @yield a block yielding a new instance of Dry::Schema::Params
39
- # subclass, allowing to define the schema inline. When the
40
- # +contract+ parameter is a schema, it will be used as a parent. Optional.
41
- def contract(contract = nil, &block)
42
- raise ArgumentError, 'Either contract or block must be provided' unless contract || block
43
- raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
44
-
45
- Grape::Validations::ContractScope.new(self, contract, &block)
46
- end
47
49
  end
48
50
  end
49
51
  end
@@ -30,7 +30,16 @@ module Grape
30
30
 
31
31
  def run_before_each(endpoint)
32
32
  superclass.run_before_each(endpoint) unless self == Endpoint
33
- before_each.each { |blk| blk.try(:call, endpoint) }
33
+ before_each.each { |blk| blk.call(endpoint) }
34
+ end
35
+
36
+ def block_to_unbound_method(block)
37
+ return unless block
38
+
39
+ define_method :temp_unbound_method, block
40
+ method = instance_method(:temp_unbound_method)
41
+ remove_method :temp_unbound_method
42
+ method
34
43
  end
35
44
  end
36
45
 
@@ -46,10 +55,7 @@ module Grape
46
55
  # @note This happens at the time of API definition, so in this context the
47
56
  # endpoint does not know if it will be mounted under a different endpoint.
48
57
  # @yield a block defining what your API should do when this endpoint is hit
49
- def initialize(new_settings, options = {}, &block)
50
- require_option(options, :path)
51
- require_option(options, :method)
52
-
58
+ def initialize(new_settings, **options, &block)
53
59
  self.inheritable_setting = new_settings.point_in_time_copy
54
60
 
55
61
  # now +namespace_stackable(:declared_params)+ contains all params defined for
@@ -65,16 +71,13 @@ module Grape
65
71
 
66
72
  @options[:path] = Array(options[:path])
67
73
  @options[:path] << '/' if options[:path].empty?
68
-
69
74
  @options[:method] = Array(options[:method])
70
- @options[:route_options] ||= {}
71
75
 
72
- @lazy_initialize_lock = Mutex.new
73
- @lazy_initialized = nil
74
76
  @status = nil
75
77
  @stream = nil
76
78
  @body = nil
77
- @source = block
79
+ @source = self.class.block_to_unbound_method(block)
80
+ @before_filter_passed = false
78
81
  end
79
82
 
80
83
  # Update our settings from a given set of stackable parameters. Used when
@@ -88,10 +91,6 @@ module Grape
88
91
  endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
89
92
  end
90
93
 
91
- def require_option(options, key)
92
- raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
93
- end
94
-
95
94
  def routes
96
95
  @routes ||= endpoints&.collect(&:routes)&.flatten || to_routes
97
96
  end
@@ -103,9 +102,13 @@ module Grape
103
102
  end
104
103
 
105
104
  def mount_in(router)
106
- return endpoints.each { |e| e.mount_in(router) } if endpoints
105
+ if endpoints
106
+ compile!
107
+ return endpoints.each { |e| e.mount_in(router) }
108
+ end
107
109
 
108
110
  reset_routes!
111
+ compile!
109
112
  routes.each do |route|
110
113
  router.append(route.apply(self))
111
114
  next unless !inheritable_setting.namespace_inheritable[:do_not_route_head] && route.request_method == Rack::GET
@@ -117,59 +120,11 @@ module Grape
117
120
  end
118
121
  end
119
122
 
120
- def to_routes
121
- default_route_options = prepare_default_route_attributes
122
-
123
- map_routes do |method, raw_path|
124
- prepared_path = Path.new(raw_path, namespace, prepare_default_path_settings)
125
- params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
126
- route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
127
- route.apply(self)
128
- end.flatten
129
- end
130
-
131
- def prepare_routes_requirements
132
- {}.merge!(*inheritable_setting.namespace_stackable[:namespace].map(&:requirements)).tap do |requirements|
133
- endpoint_requirements = options.dig(:route_options, :requirements)
134
- requirements.merge!(endpoint_requirements) if endpoint_requirements
135
- end
136
- end
137
-
138
- def prepare_default_route_attributes
139
- {
140
- namespace: namespace,
141
- version: prepare_version,
142
- requirements: prepare_routes_requirements,
143
- prefix: inheritable_setting.namespace_inheritable[:root_prefix],
144
- anchor: options[:route_options].fetch(:anchor, true),
145
- settings: inheritable_setting.route.except(:declared_params, :saved_validations),
146
- forward_match: options[:forward_match]
147
- }
148
- end
149
-
150
- def prepare_version
151
- version = inheritable_setting.namespace_inheritable[:version]
152
- return if version.blank?
153
-
154
- version.length == 1 ? version.first : version
155
- end
156
-
157
- def map_routes
158
- options[:method].map { |method| options[:path].map { |path| yield method, path } }
159
- end
160
-
161
- def prepare_default_path_settings
162
- namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
163
- namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
164
- namespace_stackable_hash.merge!(namespace_inheritable_hash)
165
- end
166
-
167
123
  def namespace
168
124
  @namespace ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace])
169
125
  end
170
126
 
171
127
  def call(env)
172
- lazy_initialize!
173
128
  dup.call!(env)
174
129
  end
175
130
 
@@ -184,7 +139,7 @@ module Grape
184
139
  # Return the collection of endpoints within this endpoint.
185
140
  # This is the case when an Grape::API mounts another Grape::API.
186
141
  def endpoints
187
- @endpoints ||= options[:app].try(:endpoints)
142
+ @endpoints ||= options[:app].respond_to?(:endpoints) ? options[:app].endpoints : nil
188
143
  end
189
144
 
190
145
  def equals?(endpoint)
@@ -208,6 +163,7 @@ module Grape
208
163
  begin
209
164
  self.class.run_before_each(self)
210
165
  run_filters befores, :before
166
+ @before_filter_passed = true
211
167
 
212
168
  if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS)
213
169
  header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
@@ -243,22 +199,7 @@ module Grape
243
199
  return unless @source
244
200
 
245
201
  ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self) do
246
- instance_exec(&@source)
247
- rescue LocalJumpError => e
248
- Grape.deprecator.warn 'Using `return` in an endpoint has been deprecated. Use `next` instead.'
249
- return e.exit_value
250
- end
251
- end
252
-
253
- def lazy_initialize!
254
- return true if @lazy_initialized
255
-
256
- @lazy_initialize_lock.synchronize do
257
- return true if @lazy_initialized
258
-
259
- @app = options[:app] || build_stack
260
- @helpers = build_helpers
261
- @lazy_initialized = true
202
+ @source.bind_call(self)
262
203
  end
263
204
  end
264
205
 
@@ -281,11 +222,11 @@ module Grape
281
222
  end
282
223
 
283
224
  def run_filters(filters, type = :other)
225
+ return unless filters
226
+
284
227
  ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
285
- filters&.each { |filter| instance_eval(&filter) }
228
+ filters.each { |filter| instance_eval(&filter) }
286
229
  end
287
- post_extension = DSL::InsideRoute.post_filter_methods(type)
288
- extend post_extension if post_extension
289
230
  end
290
231
 
291
232
  %i[befores before_validations after_validations afters finallies].each do |method|
@@ -311,6 +252,66 @@ module Grape
311
252
 
312
253
  private
313
254
 
255
+ attr_reader :before_filter_passed
256
+
257
+ def compile!
258
+ @app = options[:app] || build_stack
259
+ @helpers = build_helpers
260
+ end
261
+
262
+ def to_routes
263
+ route_options = options[:route_options]
264
+ default_route_options = prepare_default_route_attributes(route_options)
265
+ complete_route_options = route_options.merge(default_route_options)
266
+ path_settings = prepare_default_path_settings
267
+
268
+ options[:method].flat_map do |method|
269
+ options[:path].map do |path|
270
+ prepared_path = Path.new(path, default_route_options[:namespace], path_settings)
271
+ pattern = Grape::Router::Pattern.new(
272
+ origin: prepared_path.origin,
273
+ suffix: prepared_path.suffix,
274
+ anchor: default_route_options[:anchor],
275
+ params: route_options[:params],
276
+ format: options[:format],
277
+ version: default_route_options[:version],
278
+ requirements: default_route_options[:requirements]
279
+ )
280
+ Grape::Router::Route.new(self, method, pattern, complete_route_options)
281
+ end
282
+ end
283
+ end
284
+
285
+ def prepare_default_route_attributes(route_options)
286
+ {
287
+ namespace: namespace,
288
+ version: prepare_version(inheritable_setting.namespace_inheritable[:version]),
289
+ requirements: prepare_routes_requirements(route_options[:requirements]),
290
+ prefix: inheritable_setting.namespace_inheritable[:root_prefix],
291
+ anchor: route_options.fetch(:anchor, true),
292
+ settings: inheritable_setting.route.except(:declared_params, :saved_validations),
293
+ forward_match: options[:forward_match]
294
+ }
295
+ end
296
+
297
+ def prepare_default_path_settings
298
+ namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
299
+ namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
300
+ namespace_stackable_hash.merge!(namespace_inheritable_hash)
301
+ end
302
+
303
+ def prepare_routes_requirements(route_options_requirements)
304
+ namespace_requirements = inheritable_setting.namespace_stackable[:namespace].filter_map(&:requirements)
305
+ namespace_requirements << route_options_requirements if route_options_requirements.present?
306
+ namespace_requirements.reduce({}, :merge)
307
+ end
308
+
309
+ def prepare_version(namespace_inheritable_version)
310
+ return if namespace_inheritable_version.blank?
311
+
312
+ namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
313
+ end
314
+
314
315
  def build_stack
315
316
  stack = Grape::Middleware::Stack.new
316
317
 
@@ -65,7 +65,7 @@ module Grape
65
65
  end
66
66
 
67
67
  def fallback_message(key, options)
68
- if ::I18n.enforce_available_locales && ::I18n.available_locales.exclude?(FALLBACK_LOCALE)
68
+ if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE)
69
69
  key
70
70
  else
71
71
  ::I18n.translate(key, locale: FALLBACK_LOCALE, **options)
@@ -16,12 +16,12 @@ module Grape
16
16
  #
17
17
  # @param [Hash] options A hash of options.
18
18
  # @option options [String] :realm "API Authorization" The HTTP Basic realm.
19
- def http_basic(options = {}, &block)
19
+ def http_basic(options = {}, &)
20
20
  options[:realm] ||= 'API Authorization'
21
- auth :http_basic, options, &block
21
+ auth(:http_basic, options, &)
22
22
  end
23
23
 
24
- def http_digest(options = {}, &block)
24
+ def http_digest(options = {}, &)
25
25
  options[:realm] ||= 'API Authorization'
26
26
 
27
27
  if options[:realm].respond_to?(:values_at)
@@ -30,7 +30,7 @@ module Grape
30
30
  options[:opaque] ||= 'secret'
31
31
  end
32
32
 
33
- auth :http_digest, options, &block
33
+ auth(:http_digest, options, &)
34
34
  end
35
35
  end
36
36
  end
@@ -109,6 +109,10 @@ module Grape
109
109
  options
110
110
  end
111
111
  end
112
+
113
+ def try_scrub(obj)
114
+ obj.respond_to?(:valid_encoding?) && !obj.valid_encoding? ? obj.scrub : obj
115
+ end
112
116
  end
113
117
  end
114
118
  end
@@ -58,7 +58,7 @@ module Grape
58
58
  h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
59
59
  end
60
60
  backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
61
- original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
61
+ original_exception = error.is_a?(Exception) ? error : error[:original_exception]
62
62
  rack_response(status, headers, format_message(message, backtrace, original_exception))
63
63
  end
64
64
 
@@ -69,12 +69,14 @@ module Grape
69
69
  return if input.nil?
70
70
  return unless read_body_input?
71
71
 
72
- input.try(:rewind)
72
+ rewind = input.respond_to?(:rewind)
73
+
74
+ input.rewind if rewind
73
75
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
74
76
  begin
75
77
  read_rack_input(body)
76
78
  ensure
77
- input.try(:rewind)
79
+ input.rewind if rewind
78
80
  end
79
81
  end
80
82
 
@@ -130,7 +132,7 @@ module Grape
130
132
  end
131
133
 
132
134
  def format_from_extension
133
- request_path = rack_request.path.try(:scrub)
135
+ request_path = try_scrub(rack_request.path)
134
136
  dot_pos = request_path.rindex('.')
135
137
  return unless dot_pos
136
138
 
@@ -139,7 +141,7 @@ module Grape
139
141
  end
140
142
 
141
143
  def format_from_header
142
- accept_header = env['HTTP_ACCEPT'].try(:scrub)
144
+ accept_header = try_scrub(env['HTTP_ACCEPT'])
143
145
  return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
144
146
 
145
147
  media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
@@ -18,7 +18,7 @@ module Grape
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
20
  def before
21
- potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
21
+ potential_version = try_scrub(env['HTTP_ACCEPT_VERSION'])
22
22
  not_acceptable!('Accept-Version header must be set.') if strict && potential_version.blank?
23
23
 
24
24
  return if potential_version.blank?
@@ -50,6 +50,26 @@ module Grape
50
50
  def version_not_found!
51
51
  throw :error, status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER
52
52
  end
53
+
54
+ private
55
+
56
+ def available_media_types
57
+ @available_media_types ||= begin
58
+ media_types = []
59
+ base_media_type = "application/vnd.#{vendor}"
60
+ content_types.each_key do |extension|
61
+ versions&.reverse_each do |version|
62
+ media_types << "#{base_media_type}-#{version}+#{extension}"
63
+ media_types << "#{base_media_type}-#{version}"
64
+ end
65
+ media_types << "#{base_media_type}+#{extension}"
66
+ end
67
+
68
+ media_types << base_media_type
69
+ media_types.concat(content_types.values.flatten)
70
+ media_types
71
+ end
72
+ end
53
73
  end
54
74
  end
55
75
  end
@@ -101,26 +101,10 @@ module Grape
101
101
  end
102
102
 
103
103
  def version_not_found!(media_types)
104
- return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
104
+ return unless media_types.all? { |media_type| media_type&.version && versions && !versions.include?(media_type.version) }
105
105
 
106
106
  invalid_version_header!('API version not found.')
107
107
  end
108
-
109
- def available_media_types
110
- [].tap do |available_media_types|
111
- base_media_type = "application/vnd.#{vendor}"
112
- content_types.each_key do |extension|
113
- versions&.reverse_each do |version|
114
- available_media_types << "#{base_media_type}-#{version}+#{extension}"
115
- available_media_types << "#{base_media_type}-#{version}"
116
- end
117
- available_media_types << "#{base_media_type}+#{extension}"
118
- end
119
-
120
- available_media_types << base_media_type
121
- available_media_types.concat(content_types.values.flatten)
122
- end
123
- end
124
108
  end
125
109
  end
126
110
  end
@@ -22,7 +22,7 @@ module Grape
22
22
  return if path_info == '/'
23
23
 
24
24
  [mount_path, Grape::Router.normalize_path(prefix)].each do |path|
25
- path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path)
25
+ path_info = path_info.delete_prefix(path) if path.present? && path != '/' && path_info.start_with?(path)
26
26
  end
27
27
 
28
28
  slash_position = path_info.index('/', 1) # omit the first one