grape 3.2.1 → 3.3.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -9,7 +9,8 @@ module Grape
9
9
  # @param description [String] descriptive string for this endpoint
10
10
  # or namespace
11
11
  # @param options [Hash] other properties you can set to describe the
12
- # endpoint or namespace. Optional.
12
+ # endpoint or namespace. Optional. Pass these as keyword arguments;
13
+ # passing a positional options Hash is deprecated.
13
14
  # @option options :detail [String] additional detail about this endpoint
14
15
  # @option options :summary [String] summary for this endpoint
15
16
  # @option options :params [Hash] param types and info. normally, you set
@@ -49,7 +50,12 @@ module Grape
49
50
  # # ...
50
51
  # end
51
52
  #
52
- def desc(description, options = {}, &config_block)
53
+ def desc(description, *legacy_options, **options, &config_block)
54
+ if legacy_options.any?
55
+ Grape.deprecator.warn('Passing a positional options Hash to `desc` is deprecated. Pass keyword arguments instead.')
56
+ options = legacy_options.first.merge(options)
57
+ end
58
+
53
59
  settings =
54
60
  if config_block
55
61
  endpoint_config = defined?(configuration) ? configuration : nil
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Entity
6
+ # Allows you to make use of Grape Entities by setting
7
+ # the response body to the serializable hash of the
8
+ # entity provided in the `:with` option. This has the
9
+ # added benefit of automatically passing along environment
10
+ # and version information to the serialization, making it
11
+ # very easy to do conditional exposures. See Entity docs
12
+ # for more info.
13
+ #
14
+ # @param args [Array] either `(object)` or `(key, object)` where key is a Symbol
15
+ # used to nest the representation under that key in the response body.
16
+ # @param root [Symbol, String, nil] wraps the representation under this root key.
17
+ # @param with [Class, nil] the entity class to use for representation.
18
+ # If omitted, the entity class is inferred from the object via {#entity_class_for_obj}.
19
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
20
+ #
21
+ # @example
22
+ #
23
+ # get '/users/:id' do
24
+ # present User.find(params[:id]),
25
+ # with: API::Entities::User,
26
+ # admin: current_user.admin?
27
+ # end
28
+ def present(*args, root: nil, with: nil, **options)
29
+ key, object = args.count == 2 && args.first.is_a?(Symbol) ? args : [nil, args.first]
30
+ entity_class = with || entity_class_for_obj(object)
31
+ representation = entity_class ? entity_representation_for(entity_class, object, options) : object
32
+ representation = { root => representation } if root
33
+
34
+ if key
35
+ representation = body&.merge(key => representation) || { key => representation }
36
+ elsif entity_class.present? && body
37
+ raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
38
+
39
+ representation = body.merge(representation)
40
+ end
41
+
42
+ body representation
43
+ end
44
+
45
+ # Attempt to locate the Entity class for a given object, if not given
46
+ # explicitly. This is done by looking for the presence of Klass::Entity,
47
+ # where Klass is the class of the `object` parameter, or one of its
48
+ # ancestors.
49
+ # @param object [Object] the object to locate the Entity class for
50
+ # @return [Class] the located Entity class, or nil if none is found
51
+ def entity_class_for_obj(object)
52
+ klass = object_class(object)
53
+
54
+ representations = inheritable_setting.namespace_stackable_with_hash(:representations)
55
+ if representations
56
+ potential = klass.ancestors.detect { |potential| representations.key?(potential) }
57
+ return representations[potential] if potential && representations[potential]
58
+ end
59
+
60
+ return unless klass.const_defined?(:Entity)
61
+
62
+ entity = klass.const_get(:Entity)
63
+ entity if entity.respond_to?(:represent)
64
+ end
65
+
66
+ private
67
+
68
+ # Resolves the class used to look up the Entity for +object+.
69
+ # @param object [Object] the object to represent.
70
+ # @return [Class] the object's collection element class, wrapped class, or its own class.
71
+ def object_class(object)
72
+ return object.klass if object.respond_to?(:klass)
73
+ return object.first.class if object.respond_to?(:first)
74
+
75
+ object.class
76
+ end
77
+
78
+ # @param entity_class [Class] the entity class to use for representation.
79
+ # @param object [Object] the object to represent.
80
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
81
+ # @return the representation of the given object as done through the given entity_class.
82
+ def entity_representation_for(entity_class, object, options)
83
+ embeds = env.key?(Grape::Env::API_VERSION) ? { env:, version: env[Grape::Env::API_VERSION] } : { env: }
84
+ entity_class.represent(object, **embeds, **options)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -45,9 +45,8 @@ module Grape
45
45
  def include_block(block)
46
46
  return unless block
47
47
 
48
- Module.new.tap do |mod|
49
- make_inclusion(mod) { mod.class_eval(&block) }
50
- end
48
+ mod = Module.new
49
+ make_inclusion(mod) { mod.class_eval(&block) }
51
50
  end
52
51
 
53
52
  def make_inclusion(mod, &)
@@ -57,10 +56,10 @@ module Grape
57
56
  end
58
57
 
59
58
  def include_all_in_scope
60
- Module.new.tap do |mod|
61
- namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include }
62
- change!
63
- end
59
+ mod = Module.new
60
+ namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include }
61
+ change!
62
+ mod
64
63
  end
65
64
 
66
65
  def define_boolean_in_mod(mod)
@@ -72,9 +71,30 @@ module Grape
72
71
  def inject_api_helpers_to_mod(mod, &block)
73
72
  mod.extend(BaseHelper) unless mod.is_a?(BaseHelper)
74
73
  yield if block
74
+ warn_on_endpoint_overrides(mod) if Grape.config.warn_on_helper_overrides
75
75
  mod.api_changed(self)
76
76
  end
77
77
 
78
+ # When +Grape.config.warn_on_helper_overrides+ is enabled, emit a
79
+ # warning to +$stderr+ for any helper method that masks an instance
80
+ # method on +Grape::Endpoint+. Helpers are mixed into the endpoint's
81
+ # singleton class and therefore take precedence over +Endpoint+
82
+ # instance methods — usually intentional, but a common source of
83
+ # surprise when the framework gains a method that already collides
84
+ # with an existing helper name.
85
+ def warn_on_endpoint_overrides(mod)
86
+ overridden = mod.instance_methods(false).select { |m| Grape::Endpoint.method_defined?(m) }
87
+ return if overridden.empty?
88
+
89
+ overridden.each do |name|
90
+ warn(
91
+ "Grape: helper method `#{name}` overrides Grape::Endpoint##{name}. " \
92
+ 'The helper takes precedence. To use the framework implementation, remove the helper. ' \
93
+ 'Silence this warning by setting Grape.config.warn_on_helper_overrides = false.'
94
+ )
95
+ end
96
+ end
97
+
78
98
  # This module extends user defined helpers
79
99
  # to provide some API-specific functionality.
80
100
  module BaseHelper
@@ -4,6 +4,7 @@ module Grape
4
4
  module DSL
5
5
  module InsideRoute
6
6
  include Declared
7
+ include Entity
7
8
 
8
9
  # Backward compatibility: alias exception class to previous location
9
10
  MethodNotYetAvailable = Declared::MethodNotYetAvailable
@@ -14,7 +15,7 @@ module Grape
14
15
  end
15
16
 
16
17
  def configuration
17
- options[:for].configuration.evaluate
18
+ config.for.configuration.evaluate
18
19
  end
19
20
 
20
21
  # End the request and display an error to the
@@ -28,12 +29,9 @@ module Grape
28
29
  def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
29
30
  status = self.status(status || inheritable_setting.namespace_inheritable[:default_error_status])
30
31
  headers = additional_headers.present? ? header.merge(additional_headers) : header
31
- throw :error,
32
- message:,
33
- status:,
34
- headers:,
35
- backtrace:,
36
- original_exception:
32
+ throw :error, Grape::Exceptions::ErrorResponse.new(
33
+ message:, status:, headers:, backtrace:, original_exception:
34
+ )
37
35
  end
38
36
 
39
37
  # Redirect to a new url.
@@ -62,27 +60,11 @@ module Grape
62
60
  #
63
61
  # @param status [Integer] The HTTP Status Code to return for this request.
64
62
  def status(status = nil)
65
- case status
66
- when Symbol
67
- raise ArgumentError, "Status code :#{status} is invalid." unless Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(status)
63
+ return @status || default_status if status.nil?
68
64
 
65
+ case status
66
+ when Symbol, Integer
69
67
  @status = Rack::Utils.status_code(status)
70
- when Integer
71
- @status = status
72
- when nil
73
- return @status if @status
74
-
75
- if request.post?
76
- 201
77
- elsif request.delete?
78
- if @body.present?
79
- 200
80
- else
81
- 204
82
- end
83
- else
84
- 200
85
- end
86
68
  else
87
69
  raise ArgumentError, 'Status code must be Integer or Symbol.'
88
70
  end
@@ -90,11 +72,9 @@ module Grape
90
72
 
91
73
  # Set response content-type
92
74
  def content_type(val = nil)
93
- if val
94
- header(Rack::CONTENT_TYPE, val)
95
- else
96
- header[Rack::CONTENT_TYPE]
97
- end
75
+ return header(Rack::CONTENT_TYPE, val) if val
76
+
77
+ header[Rack::CONTENT_TYPE]
98
78
  end
99
79
 
100
80
  # Allows you to define the response body as something other than the
@@ -128,7 +108,6 @@ module Grape
128
108
  #
129
109
  # DELETE /12 # => 204 No Content, ""
130
110
  def return_no_content
131
- status 204
132
111
  body false
133
112
  end
134
113
 
@@ -141,14 +120,12 @@ module Grape
141
120
  #
142
121
  # GET /file # => "contents of file"
143
122
  def sendfile(value = nil)
144
- if value.is_a?(String)
145
- file_body = Grape::ServeStream::FileBody.new(value)
146
- @stream = Grape::ServeStream::StreamResponse.new(file_body)
147
- elsif !value.is_a?(NilClass)
148
- raise ArgumentError, 'Argument must be a file path'
149
- else
150
- stream
151
- end
123
+ return stream if value.nil?
124
+
125
+ raise ArgumentError, 'Argument must be a file path' unless value.is_a?(String)
126
+
127
+ file_body = Grape::ServeStream::FileBody.new(value)
128
+ @stream = Grape::ServeStream::StreamResponse.new(file_body)
152
129
  end
153
130
 
154
131
  # Allows you to define the response as a streamable object.
@@ -172,60 +149,10 @@ module Grape
172
149
  header Rack::CONTENT_LENGTH, nil
173
150
  header 'Transfer-Encoding', nil
174
151
  header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front)
175
- if value.is_a?(String)
176
- file_body = Grape::ServeStream::FileBody.new(value)
177
- @stream = Grape::ServeStream::StreamResponse.new(file_body)
178
- elsif value.respond_to?(:each)
179
- @stream = Grape::ServeStream::StreamResponse.new(value)
180
- elsif !value.is_a?(NilClass)
181
- raise ArgumentError, 'Stream object must respond to :each.'
182
- else
183
- @stream
184
- end
185
- end
186
-
187
- # Allows you to make use of Grape Entities by setting
188
- # the response body to the serializable hash of the
189
- # entity provided in the `:with` option. This has the
190
- # added benefit of automatically passing along environment
191
- # and version information to the serialization, making it
192
- # very easy to do conditional exposures. See Entity docs
193
- # for more info.
194
- #
195
- # @example
196
- #
197
- # get '/users/:id' do
198
- # present User.find(params[:id]),
199
- # with: API::Entities::User,
200
- # admin: current_user.admin?
201
- # end
202
- def present(*args, **options)
203
- key, object = if args.count == 2 && args.first.is_a?(Symbol)
204
- args
205
- else
206
- [nil, args.first]
207
- end
208
- entity_class = entity_class_for_obj(object, options)
209
-
210
- root = options.delete(:root)
211
-
212
- representation = if entity_class
213
- entity_representation_for(entity_class, object, options)
214
- else
215
- object
216
- end
217
152
 
218
- representation = { root => representation } if root
153
+ return @stream if value.nil?
219
154
 
220
- if key
221
- representation = (body || {}).merge(key => representation)
222
- elsif entity_class.present? && body
223
- raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
224
-
225
- representation = body.merge(representation)
226
- end
227
-
228
- body representation
155
+ @stream = Grape::ServeStream::StreamResponse.new(stream_body(value))
229
156
  end
230
157
 
231
158
  # Returns route information for the current request.
@@ -240,43 +167,6 @@ module Grape
240
167
  env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
241
168
  end
242
169
 
243
- # Attempt to locate the Entity class for a given object, if not given
244
- # explicitly. This is done by looking for the presence of Klass::Entity,
245
- # where Klass is the class of the `object` parameter, or one of its
246
- # ancestors.
247
- # @param object [Object] the object to locate the Entity class for
248
- # @param options [Hash]
249
- # @option options :with [Class] the explicit entity class to use
250
- # @return [Class] the located Entity class, or nil if none is found
251
- def entity_class_for_obj(object, options)
252
- entity_class = options.delete(:with)
253
- return entity_class if entity_class
254
-
255
- # entity class not explicitly defined, auto-detect from relation#klass or first object in the collection
256
- object_class = if object.respond_to?(:klass)
257
- object.klass
258
- else
259
- object.respond_to?(:first) ? object.first.class : object.class
260
- end
261
-
262
- representations = inheritable_setting.namespace_stackable_with_hash(:representations)
263
- if representations
264
- potential = object_class.ancestors.detect { |potential| representations.key?(potential) }
265
- entity_class = representations[potential] if potential
266
- end
267
-
268
- entity_class = object_class.const_get(:Entity) if !entity_class && object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
269
- entity_class
270
- end
271
-
272
- # @return the representation of the given object as done through
273
- # the given entity_class.
274
- def entity_representation_for(entity_class, object, options)
275
- embeds = { env: }
276
- embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
277
- entity_class.represent(object, **embeds, **options)
278
- end
279
-
280
170
  def http_version
281
171
  env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] }
282
172
  end
@@ -288,6 +178,25 @@ module Grape
288
178
  def context
289
179
  self
290
180
  end
181
+
182
+ private
183
+
184
+ # Wraps a stream +value+ into a body that responds to +:each+.
185
+ def stream_body(value)
186
+ return Grape::ServeStream::FileBody.new(value) if value.is_a?(String)
187
+
188
+ raise ArgumentError, 'Stream object must respond to :each.' unless value.respond_to?(:each)
189
+
190
+ value
191
+ end
192
+
193
+ # The default HTTP status when none has been set explicitly.
194
+ def default_status
195
+ return 201 if request.post?
196
+ return 204 if request.delete? && @body.blank?
197
+
198
+ 200
199
+ end
291
200
  end
292
201
  end
293
202
  end
@@ -8,11 +8,9 @@ module Grape
8
8
  # @param logger [Object] the new logger to use
9
9
  def logger(logger = nil)
10
10
  global_settings = inheritable_setting.global
11
- if logger
12
- global_settings[:logger] = logger
13
- else
14
- global_settings[:logger] || global_settings[:logger] = ::Logger.new($stdout)
15
- end
11
+ return global_settings[:logger] = logger if logger
12
+
13
+ global_settings[:logger] || global_settings[:logger] = ::Logger.new($stdout)
16
14
  end
17
15
  end
18
16
  end
@@ -122,23 +122,21 @@ module Grape
122
122
  # requires :name, type: String
123
123
  # end
124
124
  # end
125
- def requires(*attrs, **opts, &block)
125
+ def requires(*attrs, using: nil, except: nil, **opts, &block)
126
126
  opts[:presence] = { value: true, message: opts[:message] }
127
127
  opts = @group.deep_merge(opts) if @group
128
128
 
129
- if opts[:using]
130
- require_required_and_optional_fields(attrs.first, using: opts[:using], except: opts[:except])
131
- else
132
- validate_attributes(attrs, **opts, &block)
133
- block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as])
134
- end
129
+ return require_required_and_optional_fields(attrs.first, using:, except:) if using
130
+
131
+ validate_attributes(attrs, **opts, &block)
132
+ block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as])
135
133
  end
136
134
 
137
135
  # Allow, but don't require, one or more parameters for the current
138
136
  # endpoint.
139
137
  # @param (see #requires)
140
138
  # @option (see #requires)
141
- def optional(*attrs, **opts, &block)
139
+ def optional(*attrs, using: nil, except: nil, **opts, &block)
142
140
  type = opts[:type]
143
141
  opts = @group.deep_merge(opts) if @group
144
142
 
@@ -148,13 +146,10 @@ module Grape
148
146
  raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
149
147
  end
150
148
 
151
- if opts[:using]
152
- require_optional_fields(attrs.first, using: opts[:using], except: opts[:except])
153
- else
154
- validate_attributes(attrs, **opts, &block)
149
+ return require_optional_fields(attrs.first, using:, except:) if using
155
150
 
156
- block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as])
157
- end
151
+ validate_attributes(attrs, **opts, &block)
152
+ block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as])
158
153
  end
159
154
 
160
155
  # Define common settings for one or more parameters
@@ -190,35 +185,19 @@ module Grape
190
185
  # block yet.
191
186
  # @return [Boolean] whether the parameter has been defined
192
187
  def declared_param?(param)
193
- if lateral?
194
- # Elements of @declared_params of lateral scope are pushed in @parent. So check them in @parent.
195
- @parent.declared_param?(param)
196
- else
197
- # @declared_params also includes hashes of options and such, but those
198
- # won't be flattened out.
199
- @declared_params.flatten.any? do |declared_param_attr|
200
- first_hash_key_or_param(declared_param_attr.key) == param
201
- end
188
+ # Elements of @declared_params of lateral scope are pushed in @parent. So check them in @parent.
189
+ return @parent.declared_param?(param) if lateral?
190
+
191
+ # @declared_params also includes hashes of options and such, but those
192
+ # won't be flattened out.
193
+ @declared_params.flatten.any? do |declared_param_attr|
194
+ first_hash_key_or_param(declared_param_attr.key) == param
202
195
  end
203
196
  end
204
197
 
205
198
  alias group requires
206
199
 
207
- class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass
208
-
209
- def map_params(params, element, is_array = false)
210
- if params.is_a?(Array)
211
- params.map do |el|
212
- map_params(el, element, true)
213
- end
214
- elsif params.is_a?(Hash)
215
- params[element] || (@optional && is_array ? EmptyOptionalValue : {})
216
- elsif params == EmptyOptionalValue
217
- EmptyOptionalValue
218
- else
219
- {}
220
- end
221
- end
200
+ EmptyOptionalValue = Object.new.freeze
222
201
 
223
202
  # @param params [Hash] initial hash of parameters
224
203
  # @return hash of parameters relevant for the current scope
@@ -234,6 +213,21 @@ module Grape
234
213
  def first_hash_key_or_param(parameter)
235
214
  parameter.is_a?(Hash) ? parameter.keys.first : parameter
236
215
  end
216
+
217
+ def map_params(params, element, is_array: false)
218
+ case params
219
+ when Array
220
+ params.map do |el|
221
+ map_params(el, element, is_array: true)
222
+ end
223
+ when Hash
224
+ params[element] || (@optional && is_array ? EmptyOptionalValue : {})
225
+ when EmptyOptionalValue
226
+ EmptyOptionalValue
227
+ else
228
+ {}
229
+ end
230
+ end
237
231
  end
238
232
  end
239
233
  end
@@ -44,13 +44,8 @@ module Grape
44
44
  inheritable_setting.namespace_inheritable[:default_error_formatter] = new_formatter
45
45
  end
46
46
 
47
- def error_formatter(format, options)
48
- formatter = if options.is_a?(Hash) && options.key?(:with)
49
- options[:with]
50
- else
51
- options
52
- end
53
-
47
+ def error_formatter(format, options = nil, with: nil)
48
+ formatter = with || options
54
49
  inheritable_setting.namespace_stackable[:error_formatters] = { format.to_sym => formatter }
55
50
  end
56
51
 
@@ -84,46 +79,52 @@ module Grape
84
79
  # rescue_from CustomError
85
80
  # end
86
81
  #
82
+ META_RESCUE_SELECTORS = %i[all grape_exceptions internal_grape_exceptions].freeze
83
+ private_constant :META_RESCUE_SELECTORS
84
+
87
85
  # @overload rescue_from(*exception_classes, **options)
88
86
  # @param [Array] exception_classes A list of classes that you want to rescue, or
89
- # the symbol :all to rescue from all exceptions.
87
+ # one of the meta selectors +:all+, +:grape_exceptions+,
88
+ # +:internal_grape_exceptions+. Meta selectors must be used alone;
89
+ # mixing with exception classes raises +ArgumentError+.
90
90
  # @param [Block] block Execution block to handle the given exception.
91
- # @param [Hash] options Options for the rescue usage.
92
- # @option options [Boolean] :backtrace Include a backtrace in the rescue response.
93
- # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
94
- # @param [Proc] handler Execution proc to handle the given exception as an
95
- # alternative to passing a block.
96
- def rescue_from(*args, **options, &block)
97
- if args.last.is_a?(Proc)
98
- handler = args.pop
99
- elsif block
100
- handler = block
91
+ # @param [Proc] with Execution proc to handle the given exception as an alternative
92
+ # to passing a block.
93
+ # @param [Boolean] rescue_subclasses Also rescue subclasses of exception classes;
94
+ # defaults to +true+.
95
+ # @param [Boolean] backtrace Include the rescued exception's backtrace in the
96
+ # rescue response body.
97
+ # @param [Boolean] original_exception Include +inspect+ of the rescued exception
98
+ # in the rescue response body.
99
+ def rescue_from(*args, with: nil, rescue_subclasses: true, backtrace: false, original_exception: false, &block)
100
+ handler = extract_handler(args, with:, block:)
101
+ meta_selector = (args & META_RESCUE_SELECTORS).first
102
+ raise ArgumentError, "rescue_from #{meta_selector.inspect} does not accept additional arguments" if meta_selector && args.size > 1
103
+
104
+ namespace_inheritable = nil
105
+ arg = nil
106
+
107
+ if args.one?
108
+ arg = args.first
109
+ namespace_inheritable = inheritable_setting.namespace_inheritable
101
110
  end
102
111
 
103
- raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with)
104
-
105
- handler ||= extract_with(options)
106
-
107
- if args.include?(:all)
108
- inheritable_setting.namespace_inheritable[:rescue_all] = true
109
- inheritable_setting.namespace_inheritable[:all_rescue_handler] = handler
110
- elsif args.include?(:grape_exceptions)
111
- inheritable_setting.namespace_inheritable[:rescue_all] = true
112
- inheritable_setting.namespace_inheritable[:rescue_grape_exceptions] = true
113
- inheritable_setting.namespace_inheritable[:grape_exceptions_rescue_handler] = handler
112
+ case arg
113
+ when :all
114
+ namespace_inheritable[:rescue_all] = true
115
+ namespace_inheritable[:all_rescue_handler] = handler
116
+ when :grape_exceptions
117
+ namespace_inheritable[:rescue_all] = true
118
+ namespace_inheritable[:rescue_grape_exceptions] = true
119
+ namespace_inheritable[:grape_exceptions_rescue_handler] = handler
120
+ when :internal_grape_exceptions
121
+ namespace_inheritable[:internal_grape_exceptions_rescue_handler] = handler
114
122
  else
115
- handler_type =
116
- case options[:rescue_subclasses]
117
- when nil, true
118
- :rescue_handlers
119
- else
120
- :base_only_rescue_handlers
121
- end
122
-
123
- inheritable_setting.namespace_reverse_stackable[handler_type] = args.to_h { |arg| [arg, handler] }
123
+ handler_type = rescue_subclasses ? :rescue_handlers : :base_only_rescue_handlers
124
+ inheritable_setting.namespace_reverse_stackable[handler_type] = args.to_h { |klass| [klass, handler] }
124
125
  end
125
126
 
126
- inheritable_setting.namespace_stackable[:rescue_options] = options
127
+ inheritable_setting.namespace_stackable[:rescue_options] = RescueOptions.new(backtrace:, original_exception:)
127
128
  end
128
129
 
129
130
  # Allows you to specify a default representation entity for a
@@ -146,22 +147,26 @@ module Grape
146
147
  #
147
148
  # @param model_class [Class] The model class that will be represented.
148
149
  # @option options [Class] :with The entity class that will represent the model.
149
- def represent(model_class, options)
150
- raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class)
150
+ def represent(model_class, with:)
151
+ raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless with.is_a?(Class)
151
152
 
152
- inheritable_setting.namespace_stackable[:representations] = { model_class => options[:with] }
153
+ inheritable_setting.namespace_stackable[:representations] = { model_class => with }
153
154
  end
154
155
 
155
156
  private
156
157
 
157
- def extract_with(options)
158
- return unless options.key?(:with)
158
+ def extract_handler(args, with:, block:)
159
+ raise ArgumentError, 'both :with option and block cannot be passed' if block && with
159
160
 
160
- with_option = options.delete(:with)
161
- return with_option if with_option.instance_of?(Proc)
162
- return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String)
161
+ return args.pop if args.last.is_a?(Proc)
162
+ return block if block
163
+ return unless with
163
164
 
164
- raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc"
165
+ case with
166
+ when Proc, Symbol then with
167
+ when String then with.to_sym
168
+ else raise ArgumentError, "with: #{with.class}, expected Symbol, String or Proc"
169
+ end
165
170
  end
166
171
  end
167
172
  end