grape 2.4.0 → 4.0.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
data/lib/grape/env.rb CHANGED
@@ -11,10 +11,15 @@ module Grape
11
11
  API_VENDOR = 'api.vendor'
12
12
  API_FORMAT = 'api.format'
13
13
 
14
- GRAPE_REQUEST = 'grape.request'
15
- GRAPE_REQUEST_HEADERS = 'grape.request.headers'
16
- GRAPE_REQUEST_PARAMS = 'grape.request.params'
14
+ GRAPE_NORMALIZED_PATH = 'grape.normalized_path'
17
15
  GRAPE_ROUTING_ARGS = 'grape.routing_args'
18
16
  GRAPE_ALLOWED_METHODS = 'grape.allowed_methods'
17
+ GRAPE_EXCEPTION = 'grape.exception'
18
+
19
+ # Not a Grape-owned key: the de-facto convention for an exception that was
20
+ # handled rather than raised, which is how error trackers find one they
21
+ # never saw propagate. sentry-ruby, for one, collects
22
+ # +env['rack.exception'] || env['sinatra.error']+.
23
+ RACK_EXCEPTION = 'rack.exception'
19
24
  end
20
25
  end
@@ -4,34 +4,40 @@ module Grape
4
4
  module ErrorFormatter
5
5
  class Base
6
6
  class << self
7
- def call(message, backtrace, options = {}, env = nil, original_exception = nil)
8
- merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace)
9
- merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception)
10
-
11
- wrapped_message = wrap_message(present(message, env))
7
+ # Custom error formatters override +call+. The +error+ is a frozen
8
+ # {Grape::Exceptions::ErrorResponse} carrying +status+/+message+/
9
+ # +headers+/+backtrace+/+original_exception+. +env+ is the Rack env
10
+ # (needed by entity-presenter resolution). +include_backtrace+ and
11
+ # +include_original_exception+ are the request-time toggles set by
12
+ # +rescue_from+; the base implementation embeds the corresponding
13
+ # fields in the response body when they are true.
14
+ def call(error:, env: nil, include_backtrace: false, include_original_exception: false)
15
+ wrapped_message = wrap_message(present(error.message, env))
12
16
  if wrapped_message.is_a?(Hash)
13
- wrapped_message[:backtrace] = backtrace if merge_backtrace
14
- wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception
17
+ wrapped_message[:backtrace] = error.backtrace if include_backtrace && error.backtrace.present?
18
+ wrapped_message[:original_exception] = error.original_exception.inspect if include_original_exception && error.original_exception
15
19
  end
16
20
 
17
21
  format_structured_message(wrapped_message)
18
22
  end
19
23
 
20
24
  def present(message, env)
21
- present_options = {}
22
- presented_message = message
23
- if presented_message.is_a?(Hash)
24
- presented_message = presented_message.dup
25
- present_options[:with] = presented_message.delete(:with)
25
+ # error! accepts a message hash with an optional :with key specifying the entity presenter.
26
+ # Extract it here so the presenter can be resolved and the key is not serialized in the response.
27
+ # See spec/integration/grape_entity/entity_spec.rb for examples.
28
+ with = nil
29
+ payload = message
30
+ if message.is_a?(Hash) && message.key?(:with)
31
+ payload = message.dup
32
+ with = payload.delete(:with)
26
33
  end
27
34
 
28
- presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
35
+ presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(payload)
29
36
 
30
37
  unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
31
38
  # env['api.endpoint'].route does not work when the error occurs within a middleware
32
39
  # the Endpoint does not have a valid env at this moment
33
40
  http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
34
-
35
41
  found_code = http_codes.find do |http_code|
36
42
  (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
37
43
  end if env[Grape::Env::API_ENDPOINT].request
@@ -39,25 +45,25 @@ module Grape
39
45
  presenter = found_code[2] if found_code
40
46
  end
41
47
 
42
- if presenter
43
- embeds = { env: env }
44
- embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
45
- presented_message = presenter.represent(presented_message, embeds).serializable_hash
46
- end
48
+ return payload unless presenter
47
49
 
48
- presented_message
50
+ embeds = { env: }
51
+ embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
52
+ presenter.represent(payload, embeds).serializable_hash
49
53
  end
50
54
 
51
55
  def wrap_message(message)
52
56
  return message if message.is_a?(Hash)
53
57
 
54
- { message: message }
58
+ { message: }
55
59
  end
56
60
 
57
61
  def format_structured_message(_structured_message)
58
62
  raise NotImplementedError
59
63
  end
60
64
 
65
+ private
66
+
61
67
  def inherited(klass)
62
68
  super
63
69
  ErrorFormatter.register(klass)
@@ -11,6 +11,12 @@ module Grape
11
11
  private
12
12
 
13
13
  def wrap_message(message)
14
+ # Use +is_a?+ rather than +case/when+ here. +case/when Hash+ matches via
15
+ # +Module#===+, a C-level real-class check that ignores delegation, so a
16
+ # +SimpleDelegator+ wrapping a Hash (e.g. the +OutputBuilder+ returned by
17
+ # +Grape::Entity#serializable_hash+ when an error is presented via an entity)
18
+ # would fall through and be wrapped in a spurious +{ error: ... }+ envelope.
19
+ # +is_a?+ is forwarded by the delegator to the wrapped Hash, so it matches.
14
20
  return message if message.is_a?(Hash)
15
21
  return message.as_json if message.is_a?(Exceptions::ValidationErrors)
16
22
 
@@ -5,16 +5,16 @@ module Grape
5
5
  class Txt < Base
6
6
  def self.format_structured_message(structured_message)
7
7
  message = structured_message[:message] || Grape::Json.dump(structured_message)
8
- Array.wrap(message).tap do |final_message|
9
- if structured_message.key?(:backtrace)
10
- final_message << 'backtrace:'
11
- final_message.concat(structured_message[:backtrace])
12
- end
13
- if structured_message.key?(:original_exception)
14
- final_message << 'original exception:'
15
- final_message << structured_message[:original_exception]
16
- end
17
- end.join("\r\n ")
8
+ final_message = Array.wrap(message)
9
+ if structured_message.key?(:backtrace)
10
+ final_message << 'backtrace:'
11
+ final_message.concat(structured_message[:backtrace])
12
+ end
13
+ if structured_message.key?(:original_exception)
14
+ final_message << 'original exception:'
15
+ final_message << structured_message[:original_exception]
16
+ end
17
+ final_message.join("\r\n ")
18
18
  end
19
19
  end
20
20
  end
@@ -6,10 +6,14 @@ module Grape
6
6
 
7
7
  module_function
8
8
 
9
- def formatter_for(format, error_formatters = nil, default_error_formatter = nil)
9
+ # Answers nil when nothing is registered for the format, the way
10
+ # +Parser.parser_for+ does. What to fall back to then is the API's own
11
+ # +default_error_formatter+, which is the caller's state rather than the
12
+ # registry's: +Middleware::Error+ holds it and applies it.
13
+ def formatter_for(format, error_formatters = nil)
10
14
  return error_formatters[format] if error_formatters&.key?(format)
11
15
 
12
- registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt
16
+ registry[format]
13
17
  end
14
18
  end
15
19
  end
@@ -3,9 +3,9 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class Base < StandardError
6
- BASE_MESSAGES_KEY = 'grape.errors.messages'
7
- BASE_ATTRIBUTES_KEY = 'grape.errors.attributes'
8
- FALLBACK_LOCALE = :en
6
+ include Grape::Util::Translation
7
+
8
+ MESSAGE_STEPS = %w[problem summary resolution].to_h { |s| [s.to_sym, s.capitalize] }.freeze
9
9
 
10
10
  attr_reader :status, :headers
11
11
 
@@ -17,58 +17,34 @@ module Grape
17
17
  end
18
18
 
19
19
  def [](index)
20
- send index
20
+ __send__ index
21
21
  end
22
22
 
23
- protected
23
+ private
24
24
 
25
- # TODO: translate attribute first
26
- # if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned
27
- # if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution
28
- def compose_message(key, **attributes)
29
- short_message = translate_message(key, attributes)
25
+ def compose_message(key, **)
26
+ short_message = translate_message(key, **)
30
27
  return short_message unless short_message.is_a?(Hash)
31
28
 
32
- each_steps(key, attributes).with_object(+'') do |detail_array, message|
33
- message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank?
34
- end
35
- end
36
-
37
- def each_steps(key, attributes)
38
- return enum_for(:each_steps, key, attributes) unless block_given?
39
-
40
- yield 'Problem', translate_message(:"#{key}.problem", attributes)
41
- yield 'Summary', translate_message(:"#{key}.summary", attributes)
42
- yield 'Resolution', translate_message(:"#{key}.resolution", attributes)
43
- end
29
+ MESSAGE_STEPS.filter_map do |step, label|
30
+ next unless short_message.key?(step)
44
31
 
45
- def translate_attributes(keys, options = {})
46
- keys.map do |key|
47
- translate("#{BASE_ATTRIBUTES_KEY}.#{key}", options.merge(default: key.to_s))
48
- end.join(', ')
32
+ detail = translate_message(:"#{key}.#{step}", **)
33
+ "\n#{label}:\n #{detail}" if detail.present?
34
+ end.join
49
35
  end
50
36
 
51
- def translate_message(key, options = {})
52
- case key
37
+ def translate_message(translation_key, **)
38
+ case translation_key
53
39
  when Symbol
54
- translate("#{BASE_MESSAGES_KEY}.#{key}", options.merge(default: ''))
40
+ translate(translation_key, **)
41
+ when Hash
42
+ translation_key => { key:, **opts }
43
+ translate(key, **opts)
55
44
  when Proc
56
- key.call
57
- else
58
- key
59
- end
60
- end
61
-
62
- def translate(key, options)
63
- message = ::I18n.translate(key, **options)
64
- message.presence || fallback_message(key, options)
65
- end
66
-
67
- def fallback_message(key, options)
68
- if ::I18n.enforce_available_locales && ::I18n.available_locales.exclude?(FALLBACK_LOCALE)
69
- key
45
+ translation_key.call
70
46
  else
71
- ::I18n.translate(key, locale: FALLBACK_LOCALE, **options)
47
+ translation_key
72
48
  end
73
49
  end
74
50
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ # Value object representing the payload thrown via `throw :error, ...`
6
+ # and consumed by `Middleware::Error#error_response`. Replaces the
7
+ # implicit-schema Hash that previously circulated between throw sites
8
+ # and the error middleware.
9
+ ErrorResponse = Data.define(:status, :message, :headers, :backtrace, :original_exception) do
10
+ def initialize(status: nil, message: nil, headers: nil, backtrace: nil, original_exception: nil)
11
+ super
12
+ end
13
+
14
+ def to_s
15
+ "#<#{self.class.name} status=#{status.inspect} message=#{message.inspect} headers=#{headers.inspect}>"
16
+ end
17
+
18
+ # The backtrace is deliberately left unset: +Exception#backtrace+ builds
19
+ # the whole Array of location strings, and the response only renders one
20
+ # when the API asked for it. The exception travels along, so
21
+ # +Middleware::Error#error_response+ can still materialize it there.
22
+ def self.from_exception(exception)
23
+ new(
24
+ status: exception.status,
25
+ message: exception.message,
26
+ headers: exception.headers,
27
+ original_exception: exception
28
+ )
29
+ end
30
+
31
+ # Normalize heterogeneous inputs into an ErrorResponse. Preserves the
32
+ # public contract that users can still `throw :error, hash` from their
33
+ # own middleware or `rescue_from` handlers.
34
+ def self.coerce(input)
35
+ case input
36
+ when ErrorResponse
37
+ input
38
+ when Grape::Exceptions::Base
39
+ from_exception(input)
40
+ when Hash
41
+ new(**input.slice(:status, :message, :headers, :backtrace, :original_exception))
42
+ else
43
+ new
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class IncompatibleOptionValues < Base
6
6
  def initialize(option1, value1, option2, value2)
7
- super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2))
7
+ super(message: compose_message(:incompatible_option_values, option1:, value1:, option2:, value2:))
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ # Raised internally when a +rescue_from+ handler itself raises an
6
+ # unrecognised exception. The framework substitutes the original
7
+ # exception with this safe stand-in for rendering, while preserving
8
+ # the original on +env[Grape::Env::GRAPE_EXCEPTION]+ for upstream
9
+ # observability (loggers, error trackers, etc.).
10
+ class InternalServerError < Base
11
+ def initialize
12
+ super(status: 500, message: compose_message(:internal_server_error))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class InvalidAcceptHeader < Base
6
6
  def initialize(message, headers)
7
- super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers)
7
+ super(message: compose_message(:invalid_accept_header, message:), status: 406, headers:)
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class InvalidFormatter < Base
6
6
  def initialize(klass, to_format)
7
- super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format))
7
+ super(message: compose_message(:invalid_formatter, klass:, to_format:))
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class InvalidMessageBody < Base
6
6
  def initialize(body_format)
7
- super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400)
7
+ super(message: compose_message(:invalid_message_body, body_format:), status: 400)
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class InvalidVersionHeader < Base
6
6
  def initialize(message, headers)
7
- super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers)
7
+ super(message: compose_message(:invalid_version_header, message:), status: 406, headers:)
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class InvalidVersionerOption < Base
6
6
  def initialize(strategy)
7
- super(message: compose_message(:invalid_versioner_option, strategy: strategy))
7
+ super(message: compose_message(:invalid_versioner_option, strategy:))
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class MethodNotAllowed < Base
6
6
  def initialize(headers)
7
- super(message: '405 Not Allowed', status: 405, headers: headers)
7
+ super(message: '405 Not Allowed', status: 405, headers:)
8
8
  end
9
9
  end
10
10
  end
@@ -9,5 +9,3 @@ module Grape
9
9
  end
10
10
  end
11
11
  end
12
-
13
- Grape::Exceptions::MissingGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::MissingGroupTypeError', 'Grape::Exceptions::MissingGroupType', Grape.deprecator)
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class MissingMimeType < Base
6
6
  def initialize(new_format)
7
- super(message: compose_message(:missing_mime_type, new_format: new_format))
7
+ super(message: compose_message(:missing_mime_type, new_format:))
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ class RequestError < Base
6
+ def initialize(status: 400)
7
+ super(message: $ERROR_INFO&.message, status:)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class UnknownAuthStrategy < Base
6
6
  def initialize(strategy:)
7
- super(message: compose_message(:unknown_auth_strategy, strategy: strategy))
7
+ super(message: compose_message(:unknown_auth_strategy, strategy:))
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ class UnknownErrorFormatter < Base
6
+ def initialize(error_formatter_type)
7
+ super(message: compose_message(:unknown_error_formatter, error_formatter_type:))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class UnknownParameter < Base
6
6
  def initialize(param)
7
- super(message: compose_message(:unknown_parameter, param: param))
7
+ super(message: compose_message(:unknown_parameter, param:))
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class UnknownParamsBuilder < Base
6
6
  def initialize(params_builder_type)
7
- super(message: compose_message(:unknown_params_builder, params_builder_type: params_builder_type))
7
+ super(message: compose_message(:unknown_params_builder, params_builder_type:))
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Exceptions
5
5
  class UnknownValidator < Base
6
6
  def initialize(validator_type)
7
- super(message: compose_message(:unknown_validator, validator_type: validator_type))
7
+ super(message: compose_message(:unknown_validator, validator_type:))
8
8
  end
9
9
  end
10
10
  end
@@ -9,5 +9,3 @@ module Grape
9
9
  end
10
10
  end
11
11
  end
12
-
13
- Grape::Exceptions::UnsupportedGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::UnsupportedGroupTypeError', 'Grape::Exceptions::UnsupportedGroupType', Grape.deprecator)
@@ -3,16 +3,26 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class Validation < Base
6
- attr_accessor :params, :message_key
6
+ EMPTY_BACKTRACE = [].freeze
7
+
8
+ attr_reader :params, :message_key
7
9
 
8
10
  def initialize(params:, message: nil, status: nil, headers: nil)
9
- @params = params
10
- if message
11
- @message_key = message if message.is_a?(Symbol)
12
- message = translate_message(message)
13
- end
11
+ @params = Array(params)
12
+ translated =
13
+ if message
14
+ @message_key = case message
15
+ when Symbol then message
16
+ when Hash then message[:key]
17
+ end
18
+ translate_message(message)
19
+ end
14
20
 
15
- super(status: status, message: message, headers: headers)
21
+ super(status:, message: translated, headers:)
22
+ # Pre-seed the backtrace so Ruby's raise skips capture. Validation errors are
23
+ # a hot path (raised per bad attribute) and end up as 400 Bad Request responses;
24
+ # backtraces here point into Grape internals and have no diagnostic value.
25
+ set_backtrace(EMPTY_BACKTRACE)
16
26
  end
17
27
 
18
28
  # Remove all the unnecessary stuff from Grape::Exceptions::Base like status
@@ -20,6 +30,14 @@ module Grape
20
30
  def as_json(*_args)
21
31
  to_s
22
32
  end
33
+
34
+ # Returns +self+ so callers (e.g. +ValidationErrors#initialize+) can treat
35
+ # a single +Validation+ and a +ValidationArrayErrors+ wrapper uniformly via
36
+ # +flat_map(&:errors)+ — Array returns flatten in, non-Array returns
37
+ # (i.e. this +self+) append as one element.
38
+ def errors
39
+ self
40
+ end
23
41
  end
24
42
  end
25
43
  end
@@ -3,11 +3,15 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class ValidationArrayErrors < Base
6
+ EMPTY_BACKTRACE = [].freeze
7
+
6
8
  attr_reader :errors
7
9
 
8
10
  def initialize(errors)
9
11
  super()
10
12
  @errors = errors
13
+ # Skip backtrace capture — see Grape::Exceptions::Validation for rationale.
14
+ set_backtrace(EMPTY_BACKTRACE)
11
15
  end
12
16
  end
13
17
  end
@@ -3,24 +3,11 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class ValidationErrors < Base
6
- ERRORS_FORMAT_KEY = 'grape.errors.format'
7
- DEFAULT_ERRORS_FORMAT = '%<attributes>s %<message>s'
8
-
9
- include Enumerable
10
-
11
6
  attr_reader :errors
12
7
 
13
- def initialize(errors: [], headers: {})
14
- @errors = errors.group_by(&:params)
15
- super(message: full_messages.join(', '), status: 400, headers: headers)
16
- end
17
-
18
- def each
19
- errors.each_pair do |attribute, errors|
20
- errors.each do |error|
21
- yield attribute, error
22
- end
23
- end
8
+ def initialize(exceptions: [], headers: {})
9
+ @errors = exceptions.flat_map(&:errors).group_by(&:params)
10
+ super(message: full_messages.join(', '), status: 400, headers:)
24
11
  end
25
12
 
26
13
  def as_json(**_opts)
@@ -37,17 +24,28 @@ module Grape
37
24
  end
38
25
 
39
26
  def full_messages
40
- messages = map do |attributes, error|
41
- I18n.t(
42
- ERRORS_FORMAT_KEY,
43
- default: DEFAULT_ERRORS_FORMAT,
44
- attributes: translate_attributes(attributes),
45
- message: error.message
46
- )
27
+ messages = errors.flat_map do |attributes, errs|
28
+ errs.map do |error|
29
+ translate(
30
+ :format,
31
+ scope: 'grape.errors',
32
+ default: '%<attributes>s %<message>s',
33
+ attributes: translate_attributes(attributes),
34
+ message: error.message
35
+ )
36
+ end
47
37
  end
48
38
  messages.uniq!
49
39
  messages
50
40
  end
41
+
42
+ private
43
+
44
+ def translate_attributes(keys)
45
+ keys.map do |key|
46
+ translate(key, scope: 'grape.errors.attributes', default: key.to_s)
47
+ end.join(', ')
48
+ end
51
49
  end
52
50
  end
53
51
  end
@@ -4,6 +4,7 @@ module Grape
4
4
  module Formatter
5
5
  class Json < Base
6
6
  def self.call(object, _env)
7
+ return object.to_s if object.is_a?(Grape::PrecompiledJson)
7
8
  return object.to_json if object.respond_to?(:to_json)
8
9
 
9
10
  ::Grape::Json.dump(object)
@@ -5,6 +5,7 @@ module Grape
5
5
  class SerializableHash < Base
6
6
  class << self
7
7
  def call(object, _env)
8
+ return object.to_s if object.is_a?(Grape::PrecompiledJson)
8
9
  return object if object.is_a?(String)
9
10
  return ::Grape::Json.dump(serialize(object)) if serializable?(object)
10
11
  return object.to_json if object.respond_to?(:to_json)
@@ -19,15 +20,11 @@ module Grape
19
20
  end
20
21
 
21
22
  def serialize(object)
22
- if object.respond_to? :serializable_hash
23
- object.serializable_hash
24
- elsif array_serializable?(object)
25
- object.map(&:serializable_hash)
26
- elsif object.is_a?(Hash)
27
- object.transform_values { |v| serialize(v) }
28
- else
29
- object
30
- end
23
+ return object.serializable_hash if object.respond_to?(:serializable_hash)
24
+ return object.map(&:serializable_hash) if array_serializable?(object)
25
+ return object.transform_values { |v| serialize(v) } if object.is_a?(Hash)
26
+
27
+ object
31
28
  end
32
29
 
33
30
  def array_serializable?(object)