grape 2.0.0 → 2.2.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +364 -317
  4. data/UPGRADING.md +205 -7
  5. data/grape.gemspec +7 -7
  6. data/lib/grape/api/instance.rb +14 -11
  7. data/lib/grape/api.rb +19 -10
  8. data/lib/grape/content_types.rb +13 -10
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/helpers.rb +7 -3
  14. data/lib/grape/dsl/inside_route.rb +51 -15
  15. data/lib/grape/dsl/parameters.rb +5 -4
  16. data/lib/grape/dsl/request_response.rb +14 -18
  17. data/lib/grape/dsl/routing.rb +20 -4
  18. data/lib/grape/dsl/validations.rb +13 -0
  19. data/lib/grape/endpoint.rb +43 -35
  20. data/lib/grape/{util/env.rb → env.rb} +0 -5
  21. data/lib/grape/error_formatter/json.rb +13 -4
  22. data/lib/grape/error_formatter/txt.rb +11 -10
  23. data/lib/grape/error_formatter.rb +13 -25
  24. data/lib/grape/exceptions/base.rb +3 -3
  25. data/lib/grape/exceptions/validation.rb +0 -2
  26. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  27. data/lib/grape/exceptions/validation_errors.rb +2 -4
  28. data/lib/grape/extensions/hash.rb +5 -1
  29. data/lib/grape/formatter.rb +15 -25
  30. data/lib/grape/http/headers.rb +18 -34
  31. data/lib/grape/{util/json.rb → json.rb} +1 -3
  32. data/lib/grape/locale/en.yml +4 -0
  33. data/lib/grape/middleware/auth/base.rb +0 -2
  34. data/lib/grape/middleware/auth/dsl.rb +0 -2
  35. data/lib/grape/middleware/base.rb +14 -15
  36. data/lib/grape/middleware/error.rb +61 -54
  37. data/lib/grape/middleware/formatter.rb +18 -15
  38. data/lib/grape/middleware/globals.rb +1 -3
  39. data/lib/grape/middleware/stack.rb +4 -5
  40. data/lib/grape/middleware/versioner/accept_version_header.rb +8 -33
  41. data/lib/grape/middleware/versioner/header.rb +62 -123
  42. data/lib/grape/middleware/versioner/param.rb +5 -23
  43. data/lib/grape/middleware/versioner/path.rb +11 -33
  44. data/lib/grape/middleware/versioner.rb +5 -14
  45. data/lib/grape/middleware/versioner_helpers.rb +75 -0
  46. data/lib/grape/namespace.rb +3 -4
  47. data/lib/grape/parser.rb +8 -24
  48. data/lib/grape/path.rb +24 -29
  49. data/lib/grape/request.rb +4 -12
  50. data/lib/grape/router/base_route.rb +39 -0
  51. data/lib/grape/router/greedy_route.rb +20 -0
  52. data/lib/grape/router/pattern.rb +39 -30
  53. data/lib/grape/router/route.rb +22 -59
  54. data/lib/grape/router.rb +32 -37
  55. data/lib/grape/util/base_inheritable.rb +4 -4
  56. data/lib/grape/util/cache.rb +0 -3
  57. data/lib/grape/util/endpoint_configuration.rb +1 -1
  58. data/lib/grape/util/header.rb +13 -0
  59. data/lib/grape/util/inheritable_values.rb +0 -2
  60. data/lib/grape/util/lazy/block.rb +29 -0
  61. data/lib/grape/util/lazy/object.rb +45 -0
  62. data/lib/grape/util/lazy/value.rb +38 -0
  63. data/lib/grape/util/lazy/value_array.rb +21 -0
  64. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  65. data/lib/grape/util/lazy/value_hash.rb +21 -0
  66. data/lib/grape/util/media_type.rb +70 -0
  67. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  68. data/lib/grape/util/stackable_values.rb +1 -6
  69. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  70. data/lib/grape/validations/attributes_doc.rb +38 -36
  71. data/lib/grape/validations/attributes_iterator.rb +1 -0
  72. data/lib/grape/validations/contract_scope.rb +71 -0
  73. data/lib/grape/validations/params_scope.rb +22 -19
  74. data/lib/grape/validations/types/array_coercer.rb +0 -2
  75. data/lib/grape/validations/types/build_coercer.rb +69 -71
  76. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  77. data/lib/grape/validations/types/json.rb +0 -2
  78. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  79. data/lib/grape/validations/types/set_coercer.rb +0 -3
  80. data/lib/grape/validations/types.rb +0 -3
  81. data/lib/grape/validations/validators/base.rb +1 -0
  82. data/lib/grape/validations/validators/default_validator.rb +5 -1
  83. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  84. data/lib/grape/validations/validators/length_validator.rb +49 -0
  85. data/lib/grape/validations/validators/values_validator.rb +6 -1
  86. data/lib/grape/validations.rb +3 -7
  87. data/lib/grape/version.rb +1 -1
  88. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  89. data/lib/grape.rb +30 -274
  90. metadata +31 -38
  91. data/lib/grape/eager_load.rb +0 -20
  92. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  93. data/lib/grape/router/attribute_translator.rb +0 -63
  94. data/lib/grape/util/lazy_block.rb +0 -27
  95. data/lib/grape/util/lazy_object.rb +0 -43
  96. data/lib/grape/util/lazy_value.rb +0 -91
  97. data/lib/grape/util/registrable.rb +0 -15
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module DSL
7
5
  module InsideRoute
@@ -31,11 +29,17 @@ module Grape
31
29
  options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
32
30
  declared_params ||= optioned_declared_params(**options)
33
31
 
34
- if passed_params.is_a?(Array)
35
- declared_array(passed_params, options, declared_params, params_nested_path)
36
- else
37
- declared_hash(passed_params, options, declared_params, params_nested_path)
32
+ res = if passed_params.is_a?(Array)
33
+ declared_array(passed_params, options, declared_params, params_nested_path)
34
+ else
35
+ declared_hash(passed_params, options, declared_params, params_nested_path)
36
+ end
37
+
38
+ if (key_maps = namespace_stackable(:contract_key_map))
39
+ key_maps.each { |key_map| key_map.write(passed_params, res) }
38
40
  end
41
+
42
+ res
39
43
  end
40
44
 
41
45
  private
@@ -99,7 +103,7 @@ module Grape
99
103
 
100
104
  route_options_params = options[:route_options][:params] || {}
101
105
  type = route_options_params.dig(key, :type)
102
- has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) }
106
+ has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103
107
 
104
108
  if type == 'Hash' && !has_children
105
109
  {}
@@ -159,12 +163,37 @@ module Grape
159
163
  # end user with the specified message.
160
164
  #
161
165
  # @param message [String] The message to display.
162
- # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set.
166
+ # @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set.
163
167
  # @param additional_headers [Hash] Addtional headers for the response.
164
- def error!(message, status = nil, additional_headers = nil)
165
- self.status(status || namespace_inheritable(:default_error_status))
168
+ # @param backtrace [Array<String>] The backtrace of the exception that caused the error.
169
+ # @param original_exception [Exception] The original exception that caused the error.
170
+ def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
171
+ status = self.status(status || namespace_inheritable(:default_error_status))
166
172
  headers = additional_headers.present? ? header.merge(additional_headers) : header
167
- throw :error, message: message, status: self.status, headers: headers
173
+ throw :error,
174
+ message: message,
175
+ status: status,
176
+ headers: headers,
177
+ backtrace: backtrace,
178
+ original_exception: original_exception
179
+ end
180
+
181
+ # Creates a Rack response based on the provided message, status, and headers.
182
+ # The content type in the headers is set to the default content type unless provided.
183
+ # The message is HTML-escaped if the content type is 'text/html'.
184
+ #
185
+ # @param message [String] The content of the response.
186
+ # @param status [Integer] The HTTP status code.
187
+ # @params headers [Hash] (optional) Headers for the response
188
+ # (default: {Rack::CONTENT_TYPE => content_type}).
189
+ #
190
+ # Returns:
191
+ # A Rack::Response object containing the specified message, status, and headers.
192
+ #
193
+ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type })
194
+ Grape.deprecator.warn('The rack_response method has been deprecated, use error! instead.')
195
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
196
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers)
168
197
  end
169
198
 
170
199
  # Redirect to a new url.
@@ -178,7 +207,7 @@ module Grape
178
207
  if permanent
179
208
  status 301
180
209
  body_message ||= "This resource has been moved permanently to #{url}."
181
- elsif env[Grape::Http::Headers::HTTP_VERSION] == 'HTTP/1.1' && request.request_method.to_s.upcase != Grape::Http::Headers::GET
210
+ elsif http_version == 'HTTP/1.1' && !request.get?
182
211
  status 303
183
212
  body_message ||= "An alternate resource is located at #{url}."
184
213
  else
@@ -204,10 +233,9 @@ module Grape
204
233
  when nil
205
234
  return @status if instance_variable_defined?(:@status) && @status
206
235
 
207
- case request.request_method.to_s.upcase
208
- when Grape::Http::Headers::POST
236
+ if request.post?
209
237
  201
210
- when Grape::Http::Headers::DELETE
238
+ elsif request.delete?
211
239
  if instance_variable_defined?(:@body) && @body.present?
212
240
  200
213
241
  else
@@ -436,6 +464,14 @@ module Grape
436
464
  embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
437
465
  entity_class.represent(object, **embeds.merge(options))
438
466
  end
467
+
468
+ def http_version
469
+ env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL]
470
+ end
471
+
472
+ def context
473
+ self
474
+ end
439
475
  end
440
476
  end
441
477
  end
@@ -130,7 +130,7 @@ module Grape
130
130
 
131
131
  opts = attrs.extract_options!.clone
132
132
  opts[:presence] = { value: true, message: opts[:message] }
133
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
133
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
134
134
 
135
135
  if opts[:using]
136
136
  require_required_and_optional_fields(attrs.first, opts)
@@ -149,7 +149,7 @@ module Grape
149
149
 
150
150
  opts = attrs.extract_options!.clone
151
151
  type = opts[:type]
152
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
152
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
153
153
 
154
154
  # check type for optional parameter group
155
155
  if attrs && block
@@ -170,7 +170,8 @@ module Grape
170
170
  # @param (see #requires)
171
171
  # @option (see #requires)
172
172
  def with(*attrs, &block)
173
- new_group_scope(attrs.clone, &block)
173
+ new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge)
174
+ new_group_scope([new_group_attrs], &block)
174
175
  end
175
176
 
176
177
  # Disallow the given parameters to be present in the same request.
@@ -230,7 +231,7 @@ module Grape
230
231
 
231
232
  alias group requires
232
233
 
233
- class EmptyOptionalValue; end
234
+ class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass
234
235
 
235
236
  def map_params(params, element, is_array = false)
236
237
  if params.is_a?(Array)
@@ -17,18 +17,16 @@ module Grape
17
17
  # Specify the format for the API's serializers.
18
18
  # May be `:json`, `:xml`, `:txt`, etc.
19
19
  def format(new_format = nil)
20
- if new_format
21
- namespace_inheritable(:format, new_format.to_sym)
22
- # define the default error formatters
23
- namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(new_format, **{}))
24
- # define a single mime type
25
- mime_type = content_types[new_format.to_sym]
26
- raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type
27
-
28
- namespace_stackable(:content_types, new_format.to_sym => mime_type)
29
- else
30
- namespace_inheritable(:format)
31
- end
20
+ return namespace_inheritable(:format) unless new_format
21
+
22
+ symbolic_new_format = new_format.to_sym
23
+ namespace_inheritable(:format, symbolic_new_format)
24
+ namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(symbolic_new_format))
25
+
26
+ content_type = content_types[symbolic_new_format]
27
+ raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type
28
+
29
+ namespace_stackable(:content_types, symbolic_new_format => content_type)
32
30
  end
33
31
 
34
32
  # Specify a custom formatter for a content-type.
@@ -43,12 +41,10 @@ module Grape
43
41
 
44
42
  # Specify a default error formatter.
45
43
  def default_error_formatter(new_formatter_name = nil)
46
- if new_formatter_name
47
- new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name, **{})
48
- namespace_inheritable(:default_error_formatter, new_formatter)
49
- else
50
- namespace_inheritable(:default_error_formatter)
51
- end
44
+ return namespace_inheritable(:default_error_formatter) unless new_formatter_name
45
+
46
+ new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name)
47
+ namespace_inheritable(:default_error_formatter, new_formatter)
52
48
  end
53
49
 
54
50
  def error_formatter(format, options)
@@ -30,7 +30,7 @@ module Grape
30
30
  if args.any?
31
31
  options = args.extract_options!
32
32
  options = options.reverse_merge(using: :path)
33
- requested_versions = args.flatten
33
+ requested_versions = args.flatten.map(&:to_s)
34
34
 
35
35
  raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
36
 
@@ -54,7 +54,7 @@ module Grape
54
54
 
55
55
  # Define a root URL prefix for your entire API.
56
56
  def prefix(prefix = nil)
57
- namespace_inheritable(:root_prefix, prefix)
57
+ namespace_inheritable(:root_prefix, prefix&.to_s)
58
58
  end
59
59
 
60
60
  # Create a scope without affecting the URL.
@@ -85,8 +85,8 @@ module Grape
85
85
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
86
86
  mounts.each_pair do |app, path|
87
87
  if app.respond_to?(:mount_instance)
88
- opts_with = opts.any? ? opts.shift[:with] : {}
89
- mount({ app.mount_instance(configuration: opts_with) => path })
88
+ opts_with = opts.any? ? opts.first[:with] : {}
89
+ mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
90
90
  next
91
91
  end
92
92
  in_setting = inheritable_setting
@@ -103,6 +103,15 @@ module Grape
103
103
  change!
104
104
  end
105
105
 
106
+ # When trying to mount multiple times the same endpoint, remove the previous ones
107
+ # from the list of endpoints if refresh_already_mounted parameter is true
108
+ refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
109
+ if refresh_already_mounted && !endpoints.empty?
110
+ endpoints.delete_if do |endpoint|
111
+ endpoint.options[:app].to_s == app.to_s
112
+ end
113
+ end
114
+
106
115
  endpoints << Grape::Endpoint.new(
107
116
  in_setting,
108
117
  method: :any,
@@ -225,6 +234,13 @@ module Grape
225
234
  def versions
226
235
  @versions ||= []
227
236
  end
237
+
238
+ private
239
+
240
+ def refresh_mounted_api(mounts, *opts)
241
+ opts << { refresh_already_mounted: true }
242
+ mount(mounts, *opts)
243
+ end
228
244
  end
229
245
  end
230
246
  end
@@ -38,6 +38,19 @@ module Grape
38
38
  def params(&block)
39
39
  Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
40
40
  end
41
+
42
+ # Declare the contract to be used for the endpoint's parameters.
43
+ # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
44
+ # The contract or schema to be used for validation. Optional.
45
+ # @yield a block yielding a new instance of Dry::Schema::Params
46
+ # subclass, allowing to define the schema inline. When the
47
+ # +contract+ parameter is a schema, it will be used as a parent. Optional.
48
+ def contract(contract = nil, &block)
49
+ raise ArgumentError, 'Either contract or block must be provided' unless contract || block
50
+ raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
51
+
52
+ Grape::Validations::ContractScope.new(self, contract, &block)
53
+ end
41
54
  end
42
55
  end
43
56
  end
@@ -13,8 +13,8 @@ module Grape
13
13
  attr_reader :env, :request, :headers, :params
14
14
 
15
15
  class << self
16
- def new(*args, &block)
17
- self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super
16
+ def new(...)
17
+ self == Endpoint ? Class.new(Endpoint).new(...) : super
18
18
  end
19
19
 
20
20
  def before_each(new_setup = false, &block)
@@ -55,7 +55,7 @@ module Grape
55
55
 
56
56
  proc do |endpoint_instance|
57
57
  ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
58
- method.bind(endpoint_instance).call
58
+ method.bind_call(endpoint_instance)
59
59
  end
60
60
  end
61
61
  end
@@ -114,10 +114,10 @@ module Grape
114
114
  # Update our settings from a given set of stackable parameters. Used when
115
115
  # the endpoint's API is mounted under another one.
116
116
  def inherit_settings(namespace_stackable)
117
- inheritable_setting.route[:saved_validations] += namespace_stackable[:validations]
117
+ parent_validations = namespace_stackable[:validations]
118
+ inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any?
118
119
  parent_declared_params = namespace_stackable[:declared_params]
119
-
120
- inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params
120
+ inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any?
121
121
 
122
122
  endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
123
123
  end
@@ -151,7 +151,7 @@ module Grape
151
151
  reset_routes!
152
152
  routes.each do |route|
153
153
  methods = [route.request_method]
154
- methods << Grape::Http::Headers::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
154
+ methods << Rack::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET
155
155
  methods.each do |method|
156
156
  route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method
157
157
  router.append(route.apply(self))
@@ -190,10 +190,10 @@ module Grape
190
190
  end
191
191
 
192
192
  def prepare_version
193
- version = namespace_inheritable(:version) || []
194
- return if version.empty?
193
+ version = namespace_inheritable(:version)
194
+ return if version.blank?
195
195
 
196
- version.length == 1 ? version.first.to_s : version
196
+ version.length == 1 ? version.first : version
197
197
  end
198
198
 
199
199
  def merge_route_options(**default)
@@ -205,8 +205,10 @@ module Grape
205
205
  end
206
206
 
207
207
  def prepare_path(path)
208
- path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable])
209
- Path.prepare(path, namespace, path_settings)
208
+ namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
209
+ namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
210
+ path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash)
211
+ Path.new(path, namespace, path_settings)
210
212
  end
211
213
 
212
214
  def namespace
@@ -230,15 +232,24 @@ module Grape
230
232
  options[:app].endpoints if options[:app].respond_to?(:endpoints)
231
233
  end
232
234
 
233
- def equals?(e)
234
- (options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash)
235
+ def equals?(endpoint)
236
+ (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash)
237
+ end
238
+
239
+ # The purpose of this override is solely for stripping internals when an error occurs while calling
240
+ # an endpoint through an api. See https://github.com/ruby-grape/grape/issues/2398
241
+ # Otherwise, it calls super.
242
+ def inspect
243
+ return super unless env
244
+
245
+ "#{self.class} in '#{route.origin}' endpoint"
235
246
  end
236
247
 
237
248
  protected
238
249
 
239
250
  def run
240
251
  ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
241
- @header = {}
252
+ @header = Grape::Util::Header.new
242
253
  @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
243
254
  @params = @request.params
244
255
  @headers = @request.headers
@@ -279,36 +290,39 @@ module Grape
279
290
  def build_stack(helpers)
280
291
  stack = Grape::Middleware::Stack.new
281
292
 
293
+ content_types = namespace_stackable_with_hash(:content_types)
294
+ format = namespace_inheritable(:format)
295
+
282
296
  stack.use Rack::Head
283
297
  stack.use Class.new(Grape::Middleware::Error),
284
298
  helpers: helpers,
285
- format: namespace_inheritable(:format),
286
- content_types: namespace_stackable_with_hash(:content_types),
299
+ format: format,
300
+ content_types: content_types,
287
301
  default_status: namespace_inheritable(:default_error_status),
288
302
  rescue_all: namespace_inheritable(:rescue_all),
289
303
  rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
290
304
  default_error_formatter: namespace_inheritable(:default_error_formatter),
291
305
  error_formatters: namespace_stackable_with_hash(:error_formatters),
292
- rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
293
- rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
294
- base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
306
+ rescue_options: namespace_stackable_with_hash(:rescue_options),
307
+ rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers),
308
+ base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers),
295
309
  all_rescue_handler: namespace_inheritable(:all_rescue_handler),
296
310
  grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler)
297
311
 
298
312
  stack.concat namespace_stackable(:middleware)
299
313
 
300
- if namespace_inheritable(:version)
314
+ if namespace_inheritable(:version).present?
301
315
  stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
302
- versions: namespace_inheritable(:version)&.flatten,
316
+ versions: namespace_inheritable(:version).flatten,
303
317
  version_options: namespace_inheritable(:version_options),
304
318
  prefix: namespace_inheritable(:root_prefix),
305
319
  mount_path: namespace_stackable(:mount_path).first
306
320
  end
307
321
 
308
322
  stack.use Grape::Middleware::Formatter,
309
- format: namespace_inheritable(:format),
323
+ format: format,
310
324
  default_format: namespace_inheritable(:default_format) || :txt,
311
- content_types: namespace_stackable_with_hash(:content_types),
325
+ content_types: content_types,
312
326
  formatters: namespace_stackable_with_hash(:formatters),
313
327
  parsers: namespace_stackable_with_hash(:parsers)
314
328
 
@@ -319,7 +333,9 @@ module Grape
319
333
 
320
334
  def build_helpers
321
335
  helpers = namespace_stackable(:helpers)
322
- Module.new { helpers&.each { |mod_to_include| include mod_to_include } }
336
+ return if helpers.empty?
337
+
338
+ Module.new { helpers.each { |mod_to_include| include mod_to_include } }
323
339
  end
324
340
 
325
341
  private :build_stack, :build_helpers
@@ -338,7 +354,7 @@ module Grape
338
354
  @lazy_initialize_lock.synchronize do
339
355
  return true if @lazy_initialized
340
356
 
341
- @helpers = build_helpers.tap { |mod| self.class.send(:include, mod) }
357
+ @helpers = build_helpers&.tap { |mod| self.class.include mod }
342
358
  @app = options[:app] || build_stack(@helpers)
343
359
 
344
360
  @lazy_initialized = true
@@ -401,15 +417,7 @@ module Grape
401
417
 
402
418
  def options?
403
419
  options[:options_route_enabled] &&
404
- env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
405
- end
406
-
407
- def method_missing(name, *_args)
408
- raise NoMethodError.new("undefined method `#{name}' for #{self.class} in `#{route.origin}' endpoint")
409
- end
410
-
411
- def respond_to_missing?(method_name, include_private = false)
412
- super
420
+ env[Rack::REQUEST_METHOD] == Rack::OPTIONS
413
421
  end
414
422
  end
415
423
  end
@@ -11,11 +11,6 @@ module Grape
11
11
  API_VENDOR = 'api.vendor'
12
12
  API_FORMAT = 'api.format'
13
13
 
14
- RACK_INPUT = 'rack.input'
15
- RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
16
- RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
17
- RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
18
-
19
14
  GRAPE_REQUEST = 'grape.request'
20
15
  GRAPE_REQUEST_HEADERS = 'grape.request.headers'
21
16
  GRAPE_REQUEST_PARAMS = 'grape.request.params'
@@ -9,17 +9,18 @@ module Grape
9
9
  def call(message, backtrace, options = {}, env = nil, original_exception = nil)
10
10
  result = wrap_message(present(message, env))
11
11
 
12
- rescue_options = options[:rescue_options] || {}
13
- result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
14
- result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception
12
+ result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash)
13
+
15
14
  ::Grape::Json.dump(result)
16
15
  end
17
16
 
18
17
  private
19
18
 
20
19
  def wrap_message(message)
21
- if message.is_a?(Exceptions::ValidationErrors) || message.is_a?(Hash)
20
+ if message.is_a?(Hash)
22
21
  message
22
+ elsif message.is_a?(Exceptions::ValidationErrors)
23
+ message.as_json
23
24
  else
24
25
  { error: ensure_utf8(message) }
25
26
  end
@@ -30,6 +31,14 @@ module Grape
30
31
 
31
32
  message.encode('UTF-8', invalid: :replace, undef: :replace)
32
33
  end
34
+
35
+ def merge_rescue_options(result, backtrace, options, original_exception)
36
+ rescue_options = options[:rescue_options] || {}
37
+ result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
38
+ result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception
39
+
40
+ result
41
+ end
33
42
  end
34
43
  end
35
44
  end
@@ -10,16 +10,17 @@ module Grape
10
10
  message = present(message, env)
11
11
 
12
12
  result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message
13
- rescue_options = options[:rescue_options] || {}
14
- if rescue_options[:backtrace] && backtrace && !backtrace.empty?
15
- result += "\r\n backtrace:"
16
- result += backtrace.join("\r\n ")
17
- end
18
- if rescue_options[:original_exception] && original_exception
19
- result += "\r\n original exception:"
20
- result += "\r\n #{original_exception.inspect}"
21
- end
22
- result
13
+ Array.wrap(result).tap do |final_result|
14
+ rescue_options = options[:rescue_options] || {}
15
+ if rescue_options[:backtrace] && backtrace.present?
16
+ final_result << 'backtrace:'
17
+ final_result.concat(backtrace)
18
+ end
19
+ if rescue_options[:original_exception] && original_exception
20
+ final_result << 'original exception:'
21
+ final_result << original_exception.inspect
22
+ end
23
+ end.join("\r\n ")
23
24
  end
24
25
  end
25
26
  end
@@ -2,34 +2,22 @@
2
2
 
3
3
  module Grape
4
4
  module ErrorFormatter
5
- extend Util::Registrable
5
+ module_function
6
6
 
7
- class << self
8
- def builtin_formatters
9
- @builtin_formatters ||= {
10
- serializable_hash: Grape::ErrorFormatter::Json,
11
- json: Grape::ErrorFormatter::Json,
12
- jsonapi: Grape::ErrorFormatter::Json,
13
- txt: Grape::ErrorFormatter::Txt,
14
- xml: Grape::ErrorFormatter::Xml
15
- }
16
- end
7
+ DEFAULTS = {
8
+ serializable_hash: Grape::ErrorFormatter::Json,
9
+ json: Grape::ErrorFormatter::Json,
10
+ jsonapi: Grape::ErrorFormatter::Json,
11
+ txt: Grape::ErrorFormatter::Txt,
12
+ xml: Grape::ErrorFormatter::Xml
13
+ }.freeze
17
14
 
18
- def formatters(**options)
19
- builtin_formatters.merge(default_elements).merge!(options[:error_formatters] || {})
20
- end
15
+ def formatter_for(format, error_formatters = nil, default_error_formatter = nil)
16
+ select_formatter(error_formatters, format) || default_error_formatter || DEFAULTS[:txt]
17
+ end
21
18
 
22
- def formatter_for(api_format, **options)
23
- spec = formatters(**options)[api_format]
24
- case spec
25
- when nil
26
- options[:default_error_formatter] || Grape::ErrorFormatter::Txt
27
- when Symbol
28
- method(spec)
29
- else
30
- spec
31
- end
32
- end
19
+ def select_formatter(error_formatters, format)
20
+ error_formatters&.key?(format) ? error_formatters[format] : DEFAULTS[format]
33
21
  end
34
22
  end
35
23
  end
@@ -41,15 +41,15 @@ module Grape
41
41
  end
42
42
 
43
43
  def problem(key, **attributes)
44
- translate_message("#{key}.problem".to_sym, **attributes)
44
+ translate_message(:"#{key}.problem", **attributes)
45
45
  end
46
46
 
47
47
  def summary(key, **attributes)
48
- translate_message("#{key}.summary".to_sym, **attributes)
48
+ translate_message(:"#{key}.summary", **attributes)
49
49
  end
50
50
 
51
51
  def resolution(key, **attributes)
52
- translate_message("#{key}.resolution".to_sym, **attributes)
52
+ translate_message(:"#{key}.resolution", **attributes)
53
53
  end
54
54
 
55
55
  def translate_attributes(keys, **options)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/exceptions/base'
4
-
5
3
  module Grape
6
4
  module Exceptions
7
5
  class Validation < Grape::Exceptions::Base
@@ -6,6 +6,7 @@ module Grape
6
6
  attr_reader :errors
7
7
 
8
8
  def initialize(errors)
9
+ super()
9
10
  @errors = errors
10
11
  end
11
12
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/exceptions/base'
4
-
5
3
  module Grape
6
4
  module Exceptions
7
5
  class ValidationErrors < Grape::Exceptions::Base
8
6
  ERRORS_FORMAT_KEY = 'grape.errors.format'
9
- DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}'
7
+ DEFAULT_ERRORS_FORMAT = '%<attributes>s %<message>s'
10
8
 
11
9
  include Enumerable
12
10
 
@@ -14,7 +12,7 @@ module Grape
14
12
 
15
13
  def initialize(errors: [], headers: {}, **_options)
16
14
  @errors = errors.group_by(&:params)
17
- super message: full_messages.join(', '), status: 400, headers: headers
15
+ super(message: full_messages.join(', '), status: 400, headers: headers)
18
16
  end
19
17
 
20
18
  def each
@@ -12,8 +12,12 @@ module Grape
12
12
 
13
13
  def build_params
14
14
  rack_params.deep_dup.tap do |params|
15
- params.deep_merge!(grape_routing_args) if env.key?(Grape::Env::GRAPE_ROUTING_ARGS)
16
15
  params.deep_symbolize_keys!
16
+
17
+ if env.key?(Grape::Env::GRAPE_ROUTING_ARGS)
18
+ grape_routing_args.deep_symbolize_keys!
19
+ params.deep_merge!(grape_routing_args)
20
+ end
17
21
  end
18
22
  end
19
23
  end