grape 1.8.0 → 3.3.5

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 (312) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -1
  3. data/CONTRIBUTING.md +3 -11
  4. data/README.md +648 -552
  5. data/UPGRADING.md +803 -7
  6. data/grape.gemspec +11 -14
  7. data/lib/grape/api/instance.rb +68 -158
  8. data/lib/grape/api.rb +71 -107
  9. data/lib/grape/content_types.rb +23 -11
  10. data/lib/grape/cookies.rb +28 -25
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -6
  13. data/lib/grape/dsl/callbacks.rb +14 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +15 -63
  16. data/lib/grape/dsl/entity.rb +88 -0
  17. data/lib/grape/dsl/headers.rb +2 -2
  18. data/lib/grape/dsl/helpers.rb +83 -64
  19. data/lib/grape/dsl/inside_route.rb +56 -295
  20. data/lib/grape/dsl/logger.rb +4 -9
  21. data/lib/grape/dsl/middleware.rb +22 -40
  22. data/lib/grape/dsl/parameters.rb +50 -82
  23. data/lib/grape/dsl/request_response.rb +144 -146
  24. data/lib/grape/dsl/rescue_options.rb +24 -0
  25. data/lib/grape/dsl/routing.rb +261 -186
  26. data/lib/grape/dsl/settings.rb +29 -135
  27. data/lib/grape/dsl/validations.rb +39 -32
  28. data/lib/grape/dsl/version_options.rb +23 -0
  29. data/lib/grape/endpoint/options.rb +19 -0
  30. data/lib/grape/endpoint.rb +277 -250
  31. data/lib/grape/{util/env.rb → env.rb} +1 -8
  32. data/lib/grape/error_formatter/base.rb +56 -21
  33. data/lib/grape/error_formatter/json.rb +13 -15
  34. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  35. data/lib/grape/error_formatter/txt.rb +12 -18
  36. data/lib/grape/error_formatter/xml.rb +3 -13
  37. data/lib/grape/error_formatter.rb +5 -25
  38. data/lib/grape/exceptions/base.rb +22 -58
  39. data/lib/grape/exceptions/error_response.rb +45 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  41. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  42. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  44. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  45. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  46. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  47. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  48. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  49. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  50. data/lib/grape/exceptions/request_error.rb +11 -0
  51. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  52. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  53. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  54. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  55. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  56. data/lib/grape/exceptions/validation.rb +25 -9
  57. data/lib/grape/exceptions/validation_array_errors.rb +5 -0
  58. data/lib/grape/exceptions/validation_errors.rb +22 -26
  59. data/lib/grape/formatter/base.rb +16 -0
  60. data/lib/grape/formatter/json.rb +4 -6
  61. data/lib/grape/formatter/serializable_hash.rb +6 -10
  62. data/lib/grape/formatter/txt.rb +3 -5
  63. data/lib/grape/formatter/xml.rb +4 -6
  64. data/lib/grape/formatter.rb +7 -25
  65. data/lib/grape/json.rb +46 -0
  66. data/lib/grape/locale/en.yml +43 -42
  67. data/lib/grape/middleware/auth/base.rb +11 -33
  68. data/lib/grape/middleware/auth/dsl.rb +37 -31
  69. data/lib/grape/middleware/auth/strategies.rb +1 -2
  70. data/lib/grape/middleware/base.rb +56 -33
  71. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  72. data/lib/grape/middleware/error.rb +183 -91
  73. data/lib/grape/middleware/formatter.rb +94 -105
  74. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  75. data/lib/grape/middleware/stack.rb +27 -38
  76. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  77. data/lib/grape/middleware/versioner/base.rb +72 -0
  78. data/lib/grape/middleware/versioner/header.rb +44 -129
  79. data/lib/grape/middleware/versioner/param.rb +4 -25
  80. data/lib/grape/middleware/versioner/path.rb +13 -32
  81. data/lib/grape/middleware/versioner.rb +7 -14
  82. data/lib/grape/mountable.rb +22 -0
  83. data/lib/grape/namespace.rb +18 -12
  84. data/lib/grape/params_builder/base.rb +20 -0
  85. data/lib/grape/params_builder/hash.rb +11 -0
  86. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  87. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  88. data/lib/grape/params_builder.rb +15 -0
  89. data/lib/grape/parser/base.rb +16 -0
  90. data/lib/grape/parser/json.rb +6 -8
  91. data/lib/grape/parser/xml.rb +6 -8
  92. data/lib/grape/parser.rb +5 -23
  93. data/lib/grape/path.rb +38 -63
  94. data/lib/grape/railtie.rb +9 -0
  95. data/lib/grape/request.rb +168 -26
  96. data/lib/grape/router/base_route.rb +48 -0
  97. data/lib/grape/router/greedy_route.rb +26 -0
  98. data/lib/grape/router/mustermann_pattern.rb +44 -0
  99. data/lib/grape/router/pattern.rb +30 -34
  100. data/lib/grape/router/route.rb +28 -61
  101. data/lib/grape/router.rb +71 -86
  102. data/lib/grape/serve_stream/file_body.rb +1 -0
  103. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  104. data/lib/grape/serve_stream/stream_response.rb +1 -0
  105. data/lib/grape/testing.rb +33 -0
  106. data/lib/grape/util/api_description.rb +58 -0
  107. data/lib/grape/util/base_inheritable.rb +14 -14
  108. data/lib/grape/util/cache.rb +1 -3
  109. data/lib/grape/util/deep_freeze.rb +35 -0
  110. data/lib/grape/util/endpoint_configuration.rb +1 -1
  111. data/lib/grape/util/header.rb +13 -0
  112. data/lib/grape/util/inheritable_setting.rb +51 -27
  113. data/lib/grape/util/inheritable_values.rb +7 -5
  114. data/lib/grape/util/lazy/base.rb +16 -0
  115. data/lib/grape/util/lazy/block.rb +22 -0
  116. data/lib/grape/util/lazy/value.rb +31 -0
  117. data/lib/grape/util/lazy/value_array.rb +21 -0
  118. data/lib/grape/util/lazy/value_enumerable.rb +31 -0
  119. data/lib/grape/util/lazy/value_hash.rb +21 -0
  120. data/lib/grape/util/media_type.rb +67 -0
  121. data/lib/grape/util/path_normalizer.rb +34 -0
  122. data/lib/grape/util/registry.rb +27 -0
  123. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  124. data/lib/grape/util/stackable_values.rb +12 -14
  125. data/lib/grape/util/translation.rb +42 -0
  126. data/lib/grape/validations/attributes_iterator.rb +43 -27
  127. data/lib/grape/validations/coerce_options.rb +21 -0
  128. data/lib/grape/validations/contract_scope.rb +28 -0
  129. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  130. data/lib/grape/validations/oneof_collector.rb +39 -0
  131. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  132. data/lib/grape/validations/params_documentation.rb +52 -0
  133. data/lib/grape/validations/params_scope.rb +180 -303
  134. data/lib/grape/validations/shared_options.rb +19 -0
  135. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  136. data/lib/grape/validations/types/array_coercer.rb +4 -7
  137. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  138. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  139. data/lib/grape/validations/types/dry_type_coercer.rb +15 -28
  140. data/lib/grape/validations/types/json.rb +0 -2
  141. data/lib/grape/validations/types/primitive_coercer.rb +11 -35
  142. data/lib/grape/validations/types/set_coercer.rb +1 -4
  143. data/lib/grape/validations/types/variant_collection_coercer.rb +11 -0
  144. data/lib/grape/validations/types.rb +29 -54
  145. data/lib/grape/validations/validations_spec.rb +164 -0
  146. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  147. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  148. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  149. data/lib/grape/validations/validators/base.rb +120 -36
  150. data/lib/grape/validations/validators/coerce_validator.rb +18 -36
  151. data/lib/grape/validations/validators/contract_scope_validator.rb +45 -0
  152. data/lib/grape/validations/validators/default_validator.rb +13 -16
  153. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  154. data/lib/grape/validations/validators/except_values_validator.rb +15 -5
  155. data/lib/grape/validations/validators/length_validator.rb +48 -0
  156. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  157. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  158. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  159. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  160. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  161. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  162. data/lib/grape/validations/validators/values_validator.rb +36 -65
  163. data/lib/grape/validations.rb +8 -21
  164. data/lib/grape/version.rb +1 -2
  165. data/lib/grape/xml.rb +17 -0
  166. data/lib/grape.rb +75 -288
  167. metadata +79 -291
  168. data/lib/grape/api/helpers.rb +0 -9
  169. data/lib/grape/dsl/api.rb +0 -19
  170. data/lib/grape/dsl/configuration.rb +0 -15
  171. data/lib/grape/eager_load.rb +0 -20
  172. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  173. data/lib/grape/exceptions/missing_option.rb +0 -11
  174. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  175. data/lib/grape/exceptions/unknown_options.rb +0 -11
  176. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
  177. data/lib/grape/extensions/hash.rb +0 -22
  178. data/lib/grape/extensions/hashie/mash.rb +0 -26
  179. data/lib/grape/http/headers.rb +0 -61
  180. data/lib/grape/middleware/globals.rb +0 -16
  181. data/lib/grape/middleware/helpers.rb +0 -12
  182. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  183. data/lib/grape/router/attribute_translator.rb +0 -63
  184. data/lib/grape/types/invalid_value.rb +0 -8
  185. data/lib/grape/util/json.rb +0 -12
  186. data/lib/grape/util/lazy_block.rb +0 -27
  187. data/lib/grape/util/lazy_object.rb +0 -43
  188. data/lib/grape/util/lazy_value.rb +0 -91
  189. data/lib/grape/util/registrable.rb +0 -15
  190. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  191. data/lib/grape/util/xml.rb +0 -10
  192. data/lib/grape/validations/attributes_doc.rb +0 -58
  193. data/lib/grape/validations/types/build_coercer.rb +0 -94
  194. data/lib/grape/validations/validator_factory.rb +0 -15
  195. data/spec/grape/api/custom_validations_spec.rb +0 -213
  196. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  197. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  198. data/spec/grape/api/documentation_spec.rb +0 -59
  199. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  200. data/spec/grape/api/instance_spec.rb +0 -103
  201. data/spec/grape/api/invalid_format_spec.rb +0 -45
  202. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  203. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  204. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  205. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  206. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  207. data/spec/grape/api/recognize_path_spec.rb +0 -21
  208. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  209. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  210. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  211. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  212. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  213. data/spec/grape/api_remount_spec.rb +0 -509
  214. data/spec/grape/api_spec.rb +0 -4356
  215. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  216. data/spec/grape/dsl/desc_spec.rb +0 -98
  217. data/spec/grape/dsl/headers_spec.rb +0 -62
  218. data/spec/grape/dsl/helpers_spec.rb +0 -100
  219. data/spec/grape/dsl/inside_route_spec.rb +0 -531
  220. data/spec/grape/dsl/logger_spec.rb +0 -24
  221. data/spec/grape/dsl/middleware_spec.rb +0 -60
  222. data/spec/grape/dsl/parameters_spec.rb +0 -180
  223. data/spec/grape/dsl/request_response_spec.rb +0 -225
  224. data/spec/grape/dsl/routing_spec.rb +0 -275
  225. data/spec/grape/dsl/settings_spec.rb +0 -261
  226. data/spec/grape/dsl/validations_spec.rb +0 -55
  227. data/spec/grape/endpoint/declared_spec.rb +0 -846
  228. data/spec/grape/endpoint_spec.rb +0 -1085
  229. data/spec/grape/entity_spec.rb +0 -336
  230. data/spec/grape/exceptions/base_spec.rb +0 -81
  231. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
  232. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  233. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  234. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  235. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  236. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
  237. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  238. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  239. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  240. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  241. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
  242. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  243. data/spec/grape/exceptions/validation_spec.rb +0 -19
  244. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  245. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  246. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  247. data/spec/grape/grape_spec.rb +0 -9
  248. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  249. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  250. data/spec/grape/integration/rack_spec.rb +0 -51
  251. data/spec/grape/loading_spec.rb +0 -44
  252. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  253. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  254. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  255. data/spec/grape/middleware/base_spec.rb +0 -221
  256. data/spec/grape/middleware/error_spec.rb +0 -85
  257. data/spec/grape/middleware/exception_spec.rb +0 -294
  258. data/spec/grape/middleware/formatter_spec.rb +0 -461
  259. data/spec/grape/middleware/globals_spec.rb +0 -30
  260. data/spec/grape/middleware/stack_spec.rb +0 -155
  261. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  262. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  263. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  264. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  265. data/spec/grape/middleware/versioner_spec.rb +0 -21
  266. data/spec/grape/named_api_spec.rb +0 -19
  267. data/spec/grape/parser_spec.rb +0 -86
  268. data/spec/grape/path_spec.rb +0 -252
  269. data/spec/grape/presenters/presenter_spec.rb +0 -71
  270. data/spec/grape/request_spec.rb +0 -126
  271. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  272. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  273. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  274. data/spec/grape/util/stackable_values_spec.rb +0 -128
  275. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  276. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  277. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  278. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
  279. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  280. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
  281. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  282. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  283. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  284. data/spec/grape/validations/types_spec.rb +0 -111
  285. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  286. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  287. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  288. data/spec/grape/validations/validators/base_spec.rb +0 -38
  289. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  290. data/spec/grape/validations/validators/default_spec.rb +0 -463
  291. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  292. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  293. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  294. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  295. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  296. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  297. data/spec/grape/validations/validators/values_spec.rb +0 -733
  298. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  299. data/spec/grape/validations_spec.rb +0 -2030
  300. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  301. data/spec/integration/multi_json/json_spec.rb +0 -7
  302. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  303. data/spec/shared/deprecated_class_examples.rb +0 -16
  304. data/spec/shared/versioning_examples.rb +0 -215
  305. data/spec/spec_helper.rb +0 -52
  306. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  307. data/spec/support/chunks.rb +0 -14
  308. data/spec/support/content_type_helpers.rb +0 -15
  309. data/spec/support/endpoint_faker.rb +0 -25
  310. data/spec/support/file_streamer.rb +0 -13
  311. data/spec/support/integer_helpers.rb +0 -13
  312. data/spec/support/versioned_helpers.rb +0 -55
@@ -1,137 +1,229 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
- require 'active_support/core_ext/string/output_safety'
5
-
6
3
  module Grape
7
4
  module Middleware
8
5
  class Error < Base
9
- def default_options
10
- {
11
- default_status: 500, # default status returned on error
12
- default_message: '',
13
- format: :txt,
14
- helpers: nil,
15
- formatters: {},
16
- error_formatters: {},
17
- rescue_all: false, # true to rescue all exceptions
18
- rescue_grape_exceptions: false,
19
- rescue_subclasses: true, # rescue subclasses of exceptions listed
20
- rescue_options: {
21
- backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
22
- original_exception: false # true to display exception
23
- },
24
- rescue_handlers: {}, # rescue handler blocks
25
- base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
26
- all_rescue_handler: nil # rescue handler block to rescue from all exceptions
27
- }
28
- end
29
-
30
- def initialize(app, *options)
31
- super
32
- self.class.send(:include, @options[:helpers]) if @options[:helpers]
6
+ extend Forwardable
7
+ include PrecomputedContentTypes
8
+
9
+ Options = Data.define(
10
+ :all_rescue_handler, :base_only_rescue_handlers, :content_types,
11
+ :default_error_formatter, :default_message, :default_status,
12
+ :error_formatters, :format,
13
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
14
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
15
+ ) do
16
+ include Grape::Middleware::DeprecatedOptionsHashAccess
17
+
18
+ def initialize(
19
+ all_rescue_handler: nil, base_only_rescue_handlers: nil, content_types: nil,
20
+ default_error_formatter: nil, default_message: '', default_status: 500,
21
+ error_formatters: nil, format: :txt,
22
+ grape_exceptions_rescue_handler: nil, internal_grape_exceptions_rescue_handler: nil,
23
+ rescue_all: false, rescue_grape_exceptions: false, rescue_handlers: nil,
24
+ rescue_options: nil
25
+ )
26
+ # `rescue_options:` arrives nil from `Endpoint#error_middleware_options`
27
+ # when no `rescue_from` has been called — fall back to the documented
28
+ # defaults rather than letting nil propagate to `def_delegator
29
+ # :rescue_options, :backtrace`.
30
+ rescue_options ||= Grape::DSL::RescueOptions.new
31
+ super
32
+ end
33
33
  end
34
34
 
35
+ # @deprecated Kept as a frozen Hash representation of the {Options}
36
+ # defaults for back-compat. Will be removed in a future release.
37
+ DEFAULT_OPTIONS = Options.new.to_h.freeze
38
+
39
+ def_delegators :config,
40
+ :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
41
+ :default_message, :default_status, :error_formatters, :format,
42
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
43
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
44
+
45
+ # +:backtrace+ / +:original_exception+ on the rescue options become
46
+ # +#include_backtrace+ / +#include_original_exception+ on the middleware,
47
+ # which is what the formatter call site reads.
48
+ def_delegator :rescue_options, :backtrace, :include_backtrace
49
+ def_delegator :rescue_options, :original_exception, :include_original_exception
50
+
35
51
  def call!(env)
36
52
  @env = env
37
- begin
38
- error_response(catch(:error) do
39
- return @app.call(@env)
40
- end)
41
- rescue Exception => e # rubocop:disable Lint/RescueException
42
- handler =
43
- rescue_handler_for_base_only_class(e.class) ||
44
- rescue_handler_for_class_or_its_ancestor(e.class) ||
45
- rescue_handler_for_grape_exception(e.class) ||
46
- rescue_handler_for_any_class(e.class) ||
47
- raise
48
-
49
- run_rescue_handler(handler, e)
50
- end
53
+ error_response(catch(:error) { return @app.call(@env) })
54
+ rescue Exception => e # rubocop:disable Lint/RescueException
55
+ run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT])
51
56
  end
52
57
 
53
- def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
54
- headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type)
55
- rack_response(format_message(message, backtrace, original_exception), status, headers)
56
- end
58
+ private
57
59
 
58
- def default_rescue_handler(e)
59
- error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
60
+ def rack_response(status, headers, message)
61
+ message = Rack::Utils.escape_html(message) if html_content_type?(headers[Rack::CONTENT_TYPE])
62
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
60
63
  end
61
64
 
62
- # TODO: This method is deprecated. Refactor out.
63
- def error_response(error = {})
64
- status = error[:status] || options[:default_status]
65
- message = error[:message] || options[:default_message]
66
- headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
67
- headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
68
- backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
69
- original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
70
- rack_response(format_message(message, backtrace, original_exception), status, headers)
65
+ # Escaping must key off the media type only, case-insensitively. Comparing
66
+ # the raw header against 'text/html' would let a parameterized value such
67
+ # as 'text/html; charset=utf-8' (or a differently-cased 'Text/HTML', which
68
+ # browsers still treat as HTML) skip escaping and reflect an unescaped
69
+ # message into an HTML response. Such a header can be set from several
70
+ # places (a registered content type, or a custom Content-Type passed to
71
+ # error!/rescue_from), but they all render here.
72
+ def html_content_type?(content_type)
73
+ Grape::ContentTypes.media_type(content_type).to_s.casecmp?('text/html')
71
74
  end
72
75
 
73
- def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
74
- message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
75
- Rack::Response.new([message], Rack::Utils.status_code(status), headers)
76
+ def format_message(error)
77
+ current_format = env[Grape::Env::API_FORMAT] || format
78
+ formatter = Grape::ErrorFormatter.formatter_for(current_format, error_formatters, default_error_formatter)
79
+ return formatter.call(error:, env:, include_backtrace:, include_original_exception:) if formatter
80
+
81
+ throw :error, Grape::Exceptions::ErrorResponse.new(
82
+ status: 406,
83
+ message: "The requested format '#{current_format}' is not supported.",
84
+ backtrace: error.backtrace,
85
+ original_exception: error.original_exception
86
+ )
76
87
  end
77
88
 
78
- def format_message(message, backtrace, original_exception = nil)
79
- format = env[Grape::Env::API_FORMAT] || options[:format]
80
- formatter = Grape::ErrorFormatter.formatter_for(format, **options)
81
- throw :error,
82
- status: 406,
83
- message: "The requested format '#{format}' is not supported.",
84
- backtrace: backtrace,
85
- original_exception: original_exception unless formatter
86
- formatter.call(message, backtrace, options, env, original_exception)
89
+ def find_handler(klass)
90
+ registered_rescue_handler(klass) ||
91
+ rescue_handler_for_grape_exception(klass) ||
92
+ rescue_handler_for_any_class(klass) ||
93
+ raise
87
94
  end
88
95
 
89
- private
90
-
91
- def rescue_handler_for_base_only_class(klass)
92
- error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }
96
+ def error_response(error = nil)
97
+ raw = Grape::Exceptions::ErrorResponse.coerce(error)
98
+ headers = { Rack::CONTENT_TYPE => content_type }
99
+ headers.merge!(raw.headers) if raw.headers.is_a?(Hash)
100
+ payload = raw.with(
101
+ status: raw.status || default_status,
102
+ message: raw.message || default_message,
103
+ headers:,
104
+ backtrace: raw.backtrace || raw.original_exception&.backtrace || []
105
+ )
106
+ env[Grape::Env::API_ENDPOINT].status(payload.status) # error! may not have been called
107
+ rack_response(payload.status, payload.headers, format_message(payload))
108
+ end
93
109
 
94
- return unless error
110
+ def default_rescue_handler(exception)
111
+ error_response(
112
+ Grape::Exceptions::ErrorResponse.new(
113
+ message: exception.message,
114
+ backtrace: exception.backtrace,
115
+ original_exception: exception
116
+ )
117
+ )
118
+ end
95
119
 
96
- handler || :default_rescue_handler
120
+ def registered_rescue_handler(klass)
121
+ rescue_handler_from(base_only_rescue_handlers) { |err| klass == err } ||
122
+ rescue_handler_from(rescue_handlers) { |err| klass <= err }
97
123
  end
98
124
 
99
- def rescue_handler_for_class_or_its_ancestor(klass)
100
- error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err }
125
+ def rescue_handler_from(handlers)
126
+ error, handler = handlers&.find { |err, _handler| yield(err) }
101
127
 
102
128
  return unless error
103
129
 
104
- handler || :default_rescue_handler
130
+ handler || method(:default_rescue_handler)
105
131
  end
106
132
 
107
133
  def rescue_handler_for_grape_exception(klass)
108
134
  return unless klass <= Grape::Exceptions::Base
109
- return :error_response if klass == Grape::Exceptions::InvalidVersionHeader
110
- return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
135
+ return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
136
+ return unless rescue_grape_exceptions || !rescue_all
111
137
 
112
- options[:grape_exceptions_rescue_handler] || :error_response
138
+ grape_exceptions_rescue_handler || method(:error_response)
113
139
  end
114
140
 
115
141
  def rescue_handler_for_any_class(klass)
116
142
  return unless klass <= StandardError
117
- return unless options[:rescue_all] || options[:rescue_grape_exceptions]
143
+ return unless rescue_all || rescue_grape_exceptions
118
144
 
119
- options[:all_rescue_handler] || :default_rescue_handler
145
+ all_rescue_handler || method(:default_rescue_handler)
120
146
  end
121
147
 
122
- def run_rescue_handler(handler, error)
123
- if handler.instance_of?(Symbol)
124
- raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
125
-
126
- handler = public_method(handler)
148
+ def run_rescue_handler(handler, error, endpoint, redispatched: false)
149
+ handler = endpoint.public_method(handler) if handler.is_a?(Symbol)
150
+ response = catch(:error) do
151
+ handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
152
+ rescue StandardError => e
153
+ return redispatch(e, endpoint, redispatched)
127
154
  end
128
155
 
129
- response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
156
+ return error_response(response) if error?(response)
157
+ return response if response.is_a?(Rack::Response)
158
+
159
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
160
+ end
161
+
162
+ # Route an exception raised inside a +rescue_from+ block.
163
+ #
164
+ # * If we have already redispatched once (the redispatched handler
165
+ # itself raised), go straight to {#framework_default} — bounds the
166
+ # chain at one redispatch.
167
+ # * Else if the exception has a registered +rescue_from+ handler,
168
+ # run it.
169
+ # * Else if it's a +Grape::Exceptions::Base+ subclass, render it
170
+ # through +error_response+ with its own +status+ and +message+.
171
+ # * Else fall through to {#safe_default}, which lets the user opt
172
+ # in via +rescue_from :internal_grape_exceptions+ or, failing
173
+ # that, applies the framework default.
174
+ def redispatch(error, endpoint, already_redispatched)
175
+ return framework_default(endpoint) if already_redispatched
176
+
177
+ registered = registered_rescue_handler(error.class)
178
+
179
+ return run_rescue_handler(registered, error, endpoint, redispatched: true) if registered
180
+ return run_rescue_handler(method(:error_response), error, endpoint, redispatched: true) if error.is_a?(Grape::Exceptions::Base)
181
+
182
+ safe_default(error, endpoint)
183
+ end
184
+
185
+ # The unrecognised-error path. Exposes the original exception on
186
+ # the rack env so upstream Rack middleware (loggers, error
187
+ # trackers) can observe it. If the user registered a
188
+ # +rescue_from :internal_grape_exceptions+ handler, that handler
189
+ # runs and owns the response. Otherwise the framework renders the
190
+ # generic +InternalServerError+ — never the original exception's
191
+ # message. The framework deliberately does no logging of its own
192
+ # here; that's the application's call.
193
+ def safe_default(error, endpoint)
194
+ env[Grape::Env::GRAPE_EXCEPTION] = error
195
+ return run_rescue_handler(internal_grape_exceptions_rescue_handler, error, endpoint, redispatched: true) if internal_grape_exceptions_rescue_handler
196
+
197
+ framework_default(endpoint)
198
+ end
199
+
200
+ def framework_default(endpoint)
201
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InternalServerError.new, endpoint)
202
+ end
203
+
204
+ def error!(message, status = default_status, headers = {}, backtrace = [], original_exception = nil)
205
+ env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
206
+ merged_headers = { Rack::CONTENT_TYPE => content_type }.merge!(headers)
207
+ error = Grape::Exceptions::ErrorResponse.new(
208
+ status:, message:, headers: merged_headers, backtrace:, original_exception:
209
+ )
210
+ rack_response(status, merged_headers, format_message(error))
211
+ end
130
212
 
131
- if response.is_a?(Rack::Response)
132
- response
213
+ def error?(response)
214
+ case response
215
+ when Grape::Exceptions::ErrorResponse
216
+ true
217
+ when Hash
218
+ return false unless response.key?(:message) && response.key?(:status) && response.key?(:headers)
219
+
220
+ Grape.deprecator.warn(
221
+ 'Returning or throwing a Hash from a rescue handler is deprecated. ' \
222
+ 'Use `error!(...)` or a `Grape::Exceptions::ErrorResponse` instead.'
223
+ )
224
+ true
133
225
  else
134
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
226
+ false
135
227
  end
136
228
  end
137
229
  end
@@ -1,20 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Formatter < Base
8
- CHUNKED = 'chunked'
9
-
10
- def default_options
11
- {
12
- default_format: :txt,
13
- formatters: {},
14
- parsers: {}
15
- }
6
+ extend Forwardable
7
+ include PrecomputedContentTypes
8
+
9
+ Options = Data.define(:content_types, :default_format, :format, :formatters, :parsers) do
10
+ include Grape::Middleware::DeprecatedOptionsHashAccess
11
+
12
+ def initialize(content_types: nil, default_format: :txt, format: nil, formatters: nil, parsers: nil)
13
+ super
14
+ end
16
15
  end
17
16
 
17
+ # @deprecated Kept as a frozen Hash representation of the {Options}
18
+ # defaults for back-compat. Will be removed in a future release.
19
+ DEFAULT_OPTIONS = Options.new.to_h.freeze
20
+
21
+ ALL_MEDIA_TYPES = '*/*'
22
+
23
+ def_delegators :config, :default_format, :format, :formatters, :parsers
24
+
18
25
  def before
19
26
  negotiate_content_type
20
27
  read_body_input
@@ -23,13 +30,11 @@ module Grape
23
30
  def after
24
31
  return unless @app_response
25
32
 
26
- status, headers, bodies = *@app_response
33
+ status, headers, bodies = @app_response
27
34
 
28
- if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
29
- @app_response
30
- else
31
- build_formatted_response(status, headers, bodies)
32
- end
35
+ return [status, headers, []] if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
36
+
37
+ build_formatted_response(status, headers, bodies)
33
38
  end
34
39
 
35
40
  private
@@ -43,19 +48,28 @@ module Grape
43
48
  end
44
49
  else
45
50
  # Allow content-type to be explicitly overwritten
46
- formatter = fetch_formatter(headers, options)
47
- bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
48
- bodies.collect { |body| formatter.call(body, env) }
51
+ formatter = fetch_formatter(headers)
52
+ bodymap = instrument_format_response(formatter) do
53
+ bodies.map { |body| formatter.call(body, env) }
49
54
  end
50
55
  Rack::Response.new(bodymap, status, headers)
51
56
  end
52
57
  rescue Grape::Exceptions::InvalidFormatter => e
53
- throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
58
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 500, message: e.message, backtrace: e.backtrace, original_exception: e)
59
+ end
60
+
61
+ # Guards on +listening?+ so that with no subscriber the payload Hash and
62
+ # notification machinery are skipped and the block runs directly (no added
63
+ # allocations); the block is forwarded anonymously.
64
+ def instrument_format_response(formatter, &)
65
+ return yield unless ActiveSupport::Notifications.notifier.listening?('format_response.grape')
66
+
67
+ ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:, &)
54
68
  end
55
69
 
56
- def fetch_formatter(headers, options)
57
- api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
58
- Grape::Formatter.formatter_for(api_format, **options)
70
+ def fetch_formatter(headers)
71
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
72
+ Grape::Formatter.formatter_for(api_format, formatters)
59
73
  end
60
74
 
61
75
  # Set the content type header for the API format if it is not already present.
@@ -63,116 +77,91 @@ module Grape
63
77
  # @param headers [Hash]
64
78
  # @return [Hash]
65
79
  def ensure_content_type(headers)
66
- if headers[Grape::Http::Headers::CONTENT_TYPE]
67
- headers
68
- else
69
- headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
70
- end
71
- end
80
+ return headers if headers[Rack::CONTENT_TYPE]
72
81
 
73
- def request
74
- @request ||= Rack::Request.new(env)
82
+ headers[Rack::CONTENT_TYPE] = content_type_for(env[Grape::Env::API_FORMAT])
83
+ headers
75
84
  end
76
85
 
77
- # store read input in env['api.request.input']
78
86
  def read_body_input
79
- return unless
80
- (request.post? || request.put? || request.patch? || request.delete?) &&
81
- (!request.form_data? || !request.media_type) &&
82
- !request.parseable_data? &&
83
- (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
87
+ input = rack_request.body # reads RACK_INPUT
88
+ return if input.nil?
89
+ return unless read_body_input?
84
90
 
85
- return unless (input = env[Grape::Env::RACK_INPUT])
91
+ rewind = input.respond_to?(:rewind)
86
92
 
87
- input.rewind
93
+ input.rewind if rewind
88
94
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
89
95
  begin
90
- read_rack_input(body) if body && !body.empty?
96
+ read_rack_input(body)
91
97
  ensure
92
- input.rewind
98
+ input.rewind if rewind
93
99
  end
94
100
  end
95
101
 
96
- # store parsed input in env['api.request.body']
97
102
  def read_rack_input(body)
98
- fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
99
-
100
- throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
101
- parser = Grape::Parser.parser_for fmt, **options
102
- if parser
103
- begin
104
- body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
105
- if body.is_a?(Hash)
106
- env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH)
107
- env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
108
- else
109
- body
110
- end
111
- env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
103
+ return if body.empty?
104
+
105
+ media_type = rack_request.media_type
106
+ fmt = media_type ? mime_types[media_type] : default_format
107
+
108
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 415, message: "The provided content-type '#{media_type}' is not supported.") unless content_type_for(fmt)
109
+ parser = Grape::Parser.parser_for fmt, parsers
110
+ return env[Grape::Env::API_REQUEST_BODY] = body unless parser
111
+
112
+ begin
113
+ body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
114
+ if body.is_a?(Hash)
115
+ if (form_hash = env[Rack::RACK_REQUEST_FORM_HASH])
116
+ form_hash.merge!(body)
117
+ else
118
+ env[Rack::RACK_REQUEST_FORM_HASH] = body
112
119
  end
113
- rescue Grape::Exceptions::Base => e
114
- raise e
115
- rescue StandardError => e
116
- throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
120
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
117
121
  end
118
- else
119
- env[Grape::Env::API_REQUEST_BODY] = body
122
+ rescue Grape::Exceptions::Base => e
123
+ raise e
124
+ rescue StandardError => e
125
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
120
126
  end
121
127
  end
122
128
 
123
- def negotiate_content_type
124
- fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
125
- if content_type_for(fmt)
126
- env[Grape::Env::API_FORMAT] = fmt
127
- else
128
- throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
129
- end
129
+ # this middleware will not try to format the following content-types since Rack already handles them
130
+ # when calling Rack's `params` function
131
+ # - application/x-www-form-urlencoded
132
+ # - multipart/form-data
133
+ # - multipart/related
134
+ # - multipart/mixed
135
+ def read_body_input?
136
+ return false unless rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?
137
+ return false if rack_request.form_data? && rack_request.content_type
138
+ return false if rack_request.parseable_data?
139
+
140
+ rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked'
130
141
  end
131
142
 
132
- def format_from_extension
133
- parts = request.path.split('.')
143
+ def negotiate_content_type
144
+ fmt = format_from_extension || query_params['format'] || format || format_from_header || default_format
145
+ return env[Grape::Env::API_FORMAT] = fmt.to_sym if content_type_for(fmt)
134
146
 
135
- if parts.size > 1
136
- extension = parts.last
137
- # avoid symbol memory leak on an unknown format
138
- return extension.to_sym if content_type_for(extension)
139
- end
140
- nil
147
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, message: "The requested format '#{fmt}' is not supported.")
141
148
  end
142
149
 
143
- def format_from_params
144
- fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
145
- # avoid symbol memory leak on an unknown format
146
- return fmt.to_sym if content_type_for(fmt)
150
+ def format_from_extension
151
+ request_path = try_scrub(rack_request.path)
152
+ dot_pos = request_path.rindex('.')
153
+ return unless dot_pos
147
154
 
148
- fmt
155
+ extension = request_path[(dot_pos + 1)..]
156
+ extension if content_type_for(extension)
149
157
  end
150
158
 
151
159
  def format_from_header
152
- mime_array.each do |t|
153
- return mime_types[t] if mime_types.key?(t)
154
- end
155
- nil
156
- end
157
-
158
- def mime_array
159
- accept = env[Grape::Http::Headers::HTTP_ACCEPT]
160
- return [] unless accept
161
-
162
- accept_into_mime_and_quality = %r{
163
- (
164
- \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
165
- (?:
166
- (?:;[^,]*?)? # optionally multiple formats in a row
167
- ;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
168
- )?
169
- }x
170
-
171
- vendor_prefix_pattern = /vnd\.[^+]+\+/
160
+ accept_header = try_scrub(env['HTTP_ACCEPT'])
161
+ return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
172
162
 
173
- accept.scan(accept_into_mime_and_quality)
174
- .sort_by { |_, quality_preference| -quality_preference.to_f }
175
- .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
163
+ media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
164
+ mime_types[media_type] if media_type
176
165
  end
177
166
  end
178
167
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ # Include in a middleware subclass that needs content-type negotiation.
6
+ # Provides +content_types+ / +mime_types+ / +content_type_for+ /
7
+ # +content_type+ resolved from +config.content_types+ and
8
+ # +config.format+ — so the consuming middleware's +Options+ Data class
9
+ # must declare both fields. Warms those caches on the parent instance
10
+ # at initialization so per-request +dup+s inherit them (avoiding
11
+ # ~1 µs/request of +with_indifferent_access+ recomputation).
12
+ #
13
+ # Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
14
+ # content-type-aware helpers don't pay for them.
15
+ module PrecomputedContentTypes
16
+ def initialize(app, **options)
17
+ super
18
+ content_types
19
+ mime_types
20
+ content_types_indifferent_access
21
+ end
22
+
23
+ def content_types
24
+ @content_types ||= Grape::ContentTypes.content_types_for(config.content_types)
25
+ end
26
+
27
+ def mime_types
28
+ @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
29
+ end
30
+
31
+ def content_type_for(format)
32
+ content_types_indifferent_access[format]
33
+ end
34
+
35
+ def content_type
36
+ content_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
37
+ end
38
+
39
+ private
40
+
41
+ def content_types_indifferent_access
42
+ @content_types_indifferent_access ||= ActiveSupport::HashWithIndifferentAccess.new(content_types)
43
+ end
44
+ end
45
+ end
46
+ end