grape 1.8.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 (317) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +517 -1
  3. data/CONTRIBUTING.md +19 -11
  4. data/README.md +841 -545
  5. data/UPGRADING.md +1553 -7
  6. data/grape.gemspec +11 -14
  7. data/lib/grape/api/instance.rb +77 -160
  8. data/lib/grape/api.rb +75 -107
  9. data/lib/grape/content_types.rb +67 -9
  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 +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +25 -63
  16. data/lib/grape/dsl/entity.rb +106 -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 +69 -296
  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 +88 -94
  23. data/lib/grape/dsl/request_response.rb +143 -147
  24. data/lib/grape/dsl/rescue_options.rb +25 -0
  25. data/lib/grape/dsl/routing.rb +333 -191
  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 +25 -0
  30. data/lib/grape/endpoint.rb +319 -269
  31. data/lib/grape/{util/env.rb → env.rb} +8 -8
  32. data/lib/grape/error_formatter/base.rb +57 -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 +9 -25
  38. data/lib/grape/exceptions/base.rb +22 -58
  39. data/lib/grape/exceptions/error_response.rb +48 -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_error_formatter.rb +11 -0
  53. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  54. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  55. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  56. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  57. data/lib/grape/exceptions/validation.rb +28 -11
  58. data/lib/grape/exceptions/validation_array_errors.rb +5 -0
  59. data/lib/grape/exceptions/validation_errors.rb +22 -26
  60. data/lib/grape/formatter/base.rb +16 -0
  61. data/lib/grape/formatter/json.rb +5 -6
  62. data/lib/grape/formatter/serializable_hash.rb +7 -10
  63. data/lib/grape/formatter/txt.rb +3 -5
  64. data/lib/grape/formatter/xml.rb +4 -6
  65. data/lib/grape/formatter.rb +7 -25
  66. data/lib/grape/json.rb +46 -0
  67. data/lib/grape/locale/en.yml +44 -42
  68. data/lib/grape/middleware/auth/base.rb +11 -33
  69. data/lib/grape/middleware/auth/dsl.rb +12 -34
  70. data/lib/grape/middleware/auth/strategies.rb +1 -2
  71. data/lib/grape/middleware/base.rb +56 -33
  72. data/lib/grape/middleware/error.rb +290 -91
  73. data/lib/grape/middleware/formatter.rb +164 -106
  74. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  75. data/lib/grape/middleware/stack.rb +38 -40
  76. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  77. data/lib/grape/middleware/versioner/base.rb +66 -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 +48 -25
  81. data/lib/grape/middleware/versioner.rb +7 -14
  82. data/lib/grape/mountable.rb +22 -0
  83. data/lib/grape/namespace.rb +21 -14
  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 +8 -94
  94. data/lib/grape/precompiled_json.rb +50 -0
  95. data/lib/grape/railtie.rb +9 -0
  96. data/lib/grape/request.rb +200 -26
  97. data/lib/grape/router/base_route.rb +85 -0
  98. data/lib/grape/router/greedy_route.rb +30 -0
  99. data/lib/grape/router/mustermann_pattern.rb +44 -0
  100. data/lib/grape/router/pattern/path.rb +78 -0
  101. data/lib/grape/router/pattern.rb +77 -35
  102. data/lib/grape/router/route.rb +79 -60
  103. data/lib/grape/router.rb +128 -104
  104. data/lib/grape/serve_stream/file_body.rb +7 -0
  105. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  106. data/lib/grape/serve_stream/stream_response.rb +7 -0
  107. data/lib/grape/testing.rb +33 -0
  108. data/lib/grape/util/api_description.rb +67 -0
  109. data/lib/grape/util/cache.rb +22 -5
  110. data/lib/grape/util/deep_freeze.rb +34 -0
  111. data/lib/grape/util/endpoint_configuration.rb +1 -1
  112. data/lib/grape/util/freeze_on_new.rb +20 -0
  113. data/lib/grape/util/header.rb +13 -0
  114. data/lib/grape/util/inheritable_setting.rb +777 -39
  115. data/lib/grape/util/lazy/base.rb +16 -0
  116. data/lib/grape/util/lazy/block.rb +22 -0
  117. data/lib/grape/util/lazy/value.rb +31 -0
  118. data/lib/grape/util/lazy/value_array.rb +21 -0
  119. data/lib/grape/util/lazy/value_enumerable.rb +31 -0
  120. data/lib/grape/util/lazy/value_hash.rb +21 -0
  121. data/lib/grape/util/media_type.rb +74 -0
  122. data/lib/grape/util/path_normalizer.rb +37 -0
  123. data/lib/grape/util/registry.rb +37 -0
  124. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  125. data/lib/grape/util/stackable_values.rb +40 -16
  126. data/lib/grape/util/translation.rb +42 -0
  127. data/lib/grape/validations/attributes_iterator.rb +61 -28
  128. data/lib/grape/validations/coerce_options.rb +21 -0
  129. data/lib/grape/validations/contract_scope.rb +29 -0
  130. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  131. data/lib/grape/validations/oneof_collector.rb +35 -0
  132. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  133. data/lib/grape/validations/params_documentation.rb +52 -0
  134. data/lib/grape/validations/params_scope.rb +223 -306
  135. data/lib/grape/validations/shared_options.rb +19 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  137. data/lib/grape/validations/types/array_coercer.rb +7 -12
  138. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  140. data/lib/grape/validations/types/dry_type_coercer.rb +17 -28
  141. data/lib/grape/validations/types/json.rb +1 -5
  142. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  143. data/lib/grape/validations/types/primitive_coercer.rb +14 -35
  144. data/lib/grape/validations/types/set_coercer.rb +1 -4
  145. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  146. data/lib/grape/validations/types.rb +29 -54
  147. data/lib/grape/validations/validations_spec.rb +164 -0
  148. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  149. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  150. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  151. data/lib/grape/validations/validators/base.rb +118 -37
  152. data/lib/grape/validations/validators/coerce_validator.rb +26 -38
  153. data/lib/grape/validations/validators/contract_scope_validator.rb +46 -0
  154. data/lib/grape/validations/validators/default_validator.rb +13 -16
  155. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  156. data/lib/grape/validations/validators/except_values_validator.rb +15 -5
  157. data/lib/grape/validations/validators/length_validator.rb +50 -0
  158. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  159. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  160. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  161. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  162. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  163. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  164. data/lib/grape/validations/validators/values_validator.rb +36 -65
  165. data/lib/grape/validations.rb +8 -21
  166. data/lib/grape/version.rb +1 -2
  167. data/lib/grape/xml.rb +17 -0
  168. data/lib/grape.rb +96 -288
  169. metadata +83 -294
  170. data/lib/grape/api/helpers.rb +0 -9
  171. data/lib/grape/dsl/api.rb +0 -19
  172. data/lib/grape/dsl/configuration.rb +0 -15
  173. data/lib/grape/eager_load.rb +0 -20
  174. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  175. data/lib/grape/exceptions/missing_option.rb +0 -11
  176. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  177. data/lib/grape/exceptions/unknown_options.rb +0 -11
  178. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
  179. data/lib/grape/extensions/hash.rb +0 -22
  180. data/lib/grape/extensions/hashie/mash.rb +0 -26
  181. data/lib/grape/http/headers.rb +0 -61
  182. data/lib/grape/middleware/globals.rb +0 -16
  183. data/lib/grape/middleware/helpers.rb +0 -12
  184. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  185. data/lib/grape/router/attribute_translator.rb +0 -63
  186. data/lib/grape/types/invalid_value.rb +0 -8
  187. data/lib/grape/util/base_inheritable.rb +0 -43
  188. data/lib/grape/util/inheritable_values.rb +0 -31
  189. data/lib/grape/util/json.rb +0 -12
  190. data/lib/grape/util/lazy_block.rb +0 -27
  191. data/lib/grape/util/lazy_object.rb +0 -43
  192. data/lib/grape/util/lazy_value.rb +0 -91
  193. data/lib/grape/util/registrable.rb +0 -15
  194. data/lib/grape/util/reverse_stackable_values.rb +0 -20
  195. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  196. data/lib/grape/util/xml.rb +0 -10
  197. data/lib/grape/validations/attributes_doc.rb +0 -58
  198. data/lib/grape/validations/types/build_coercer.rb +0 -94
  199. data/lib/grape/validations/validator_factory.rb +0 -15
  200. data/spec/grape/api/custom_validations_spec.rb +0 -213
  201. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  202. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  203. data/spec/grape/api/documentation_spec.rb +0 -59
  204. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  205. data/spec/grape/api/instance_spec.rb +0 -103
  206. data/spec/grape/api/invalid_format_spec.rb +0 -45
  207. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  208. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  209. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  210. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  211. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  212. data/spec/grape/api/recognize_path_spec.rb +0 -21
  213. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  214. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  215. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  216. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  217. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  218. data/spec/grape/api_remount_spec.rb +0 -509
  219. data/spec/grape/api_spec.rb +0 -4356
  220. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  221. data/spec/grape/dsl/desc_spec.rb +0 -98
  222. data/spec/grape/dsl/headers_spec.rb +0 -62
  223. data/spec/grape/dsl/helpers_spec.rb +0 -100
  224. data/spec/grape/dsl/inside_route_spec.rb +0 -531
  225. data/spec/grape/dsl/logger_spec.rb +0 -24
  226. data/spec/grape/dsl/middleware_spec.rb +0 -60
  227. data/spec/grape/dsl/parameters_spec.rb +0 -180
  228. data/spec/grape/dsl/request_response_spec.rb +0 -225
  229. data/spec/grape/dsl/routing_spec.rb +0 -275
  230. data/spec/grape/dsl/settings_spec.rb +0 -261
  231. data/spec/grape/dsl/validations_spec.rb +0 -55
  232. data/spec/grape/endpoint/declared_spec.rb +0 -846
  233. data/spec/grape/endpoint_spec.rb +0 -1085
  234. data/spec/grape/entity_spec.rb +0 -336
  235. data/spec/grape/exceptions/base_spec.rb +0 -81
  236. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
  237. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  238. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  239. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  240. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  241. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
  242. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  243. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  244. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  245. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  246. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
  247. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  248. data/spec/grape/exceptions/validation_spec.rb +0 -19
  249. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  250. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  251. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  252. data/spec/grape/grape_spec.rb +0 -9
  253. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  254. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  255. data/spec/grape/integration/rack_spec.rb +0 -51
  256. data/spec/grape/loading_spec.rb +0 -44
  257. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  258. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  259. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  260. data/spec/grape/middleware/base_spec.rb +0 -221
  261. data/spec/grape/middleware/error_spec.rb +0 -85
  262. data/spec/grape/middleware/exception_spec.rb +0 -294
  263. data/spec/grape/middleware/formatter_spec.rb +0 -461
  264. data/spec/grape/middleware/globals_spec.rb +0 -30
  265. data/spec/grape/middleware/stack_spec.rb +0 -155
  266. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  267. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  268. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  269. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  270. data/spec/grape/middleware/versioner_spec.rb +0 -21
  271. data/spec/grape/named_api_spec.rb +0 -19
  272. data/spec/grape/parser_spec.rb +0 -86
  273. data/spec/grape/path_spec.rb +0 -252
  274. data/spec/grape/presenters/presenter_spec.rb +0 -71
  275. data/spec/grape/request_spec.rb +0 -126
  276. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  277. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  278. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  279. data/spec/grape/util/stackable_values_spec.rb +0 -128
  280. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  281. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  282. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  283. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
  284. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  285. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
  286. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  287. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  288. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  289. data/spec/grape/validations/types_spec.rb +0 -111
  290. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  291. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  292. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  293. data/spec/grape/validations/validators/base_spec.rb +0 -38
  294. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  295. data/spec/grape/validations/validators/default_spec.rb +0 -463
  296. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  297. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  298. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  299. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  300. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  301. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  302. data/spec/grape/validations/validators/values_spec.rb +0 -733
  303. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  304. data/spec/grape/validations_spec.rb +0 -2030
  305. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  306. data/spec/integration/multi_json/json_spec.rb +0 -7
  307. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  308. data/spec/shared/deprecated_class_examples.rb +0 -16
  309. data/spec/shared/versioning_examples.rb +0 -215
  310. data/spec/spec_helper.rb +0 -52
  311. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  312. data/spec/support/chunks.rb +0 -14
  313. data/spec/support/content_type_helpers.rb +0 -15
  314. data/spec/support/endpoint_faker.rb +0 -25
  315. data/spec/support/file_streamer.rb +0 -13
  316. data/spec/support/integer_helpers.rb +0 -13
  317. data/spec/support/versioned_helpers.rb +0 -55
@@ -1,62 +1,104 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
- require 'mustermann/grape'
5
- require 'grape/util/cache'
6
-
7
3
  module Grape
8
4
  class Router
9
5
  class Pattern
10
- DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze
11
- DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze
6
+ extend Forwardable
12
7
 
13
- attr_reader :origin, :path, :pattern, :to_regexp
8
+ DEFAULT_CAPTURES = %w[format version].freeze
14
9
 
15
- extend Forwardable
16
- def_delegators :pattern, :named_captures, :params
10
+ attr_reader :origin, :path, :pattern, :to_regexp, :anchor, :version, :requirements
11
+
12
+ def_delegators :pattern, :params
17
13
  def_delegators :to_regexp, :===
18
14
  alias match? ===
19
15
 
20
- def initialize(pattern, **options)
21
- @origin = pattern
22
- @path = build_path(pattern, **options)
23
- @pattern = Mustermann::Grape.new(@path, **pattern_options(options))
16
+ # Build a Pattern from a raw path, namespace and the API's inheritable
17
+ # settings. {Path} owns the settings-aware assembly of +origin+/+suffix+;
18
+ # the Pattern itself stays value-based (see {#initialize}).
19
+ def self.build(path:, namespace:, settings:, anchor:, params:, version:, requirements:)
20
+ built_path = Path.new(path, namespace, settings)
21
+ new(origin: built_path.origin, suffix: built_path.suffix, anchor:, params:, version:, requirements:)
22
+ end
23
+
24
+ def initialize(origin:, suffix:, anchor:, params:, version:, requirements:)
25
+ @origin = origin
26
+ @anchor = anchor
27
+ @version = version
28
+ @requirements = requirements
29
+ @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
30
+ @pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(version, requirements))
24
31
  @to_regexp = @pattern.to_regexp
32
+ @captures = @to_regexp.names.any?
25
33
  end
26
34
 
27
- private
35
+ # True when the compiled pattern has named captures to extract from a
36
+ # matched path. A fully static path has none — not even +format+, which
37
+ # the suffix spells as a literal — so asking Mustermann for its params
38
+ # would run the regexp a second time (the router's union already matched
39
+ # it) only to hand back an empty Hash.
40
+ #
41
+ # Resolved once here rather than per request: +Regexp#names+ builds an
42
+ # Array and a String per capture, and Ruby offers no predicate that skips
43
+ # that (+named_captures+ builds a Hash, and scanning the source for
44
+ # <tt>(?<</tt> would count lookbehinds, which name nothing).
45
+ def captures?
46
+ @captures
47
+ end
28
48
 
29
- def pattern_options(options)
30
- capture = extract_capture(**options)
31
- options = DEFAULT_PATTERN_OPTIONS.dup
32
- options[:capture] = capture if capture.present?
33
- options
49
+ def captures_default
50
+ to_regexp.names
51
+ .delete_if { |n| DEFAULT_CAPTURES.include?(n) }
52
+ .to_h { |k| [k, ''] }
34
53
  end
35
54
 
36
- def build_path(pattern, anchor: false, suffix: nil, **_options)
37
- unless anchor || pattern.end_with?('*path')
38
- pattern = +pattern
39
- pattern << '/' unless pattern.end_with?('/')
40
- pattern << '*path'
41
- end
55
+ private
42
56
 
43
- pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = "?#{parts.last}"
45
- end.join('/') if pattern.end_with?('*path')
57
+ # The declared versions constrain the +:version+ capture. They are handed
58
+ # to Mustermann as one alternation Regexp rather than as the Array of
59
+ # Strings they arrive in, because an Array capture makes Mustermann build
60
+ # a *converter* for the capture -- and for an Array of plain Strings that
61
+ # converter is the identity function (Mustermann only derives one from a
62
+ # Class or a Symbol, so every entry contributes nothing and the lambda
63
+ # falls through to `|| string`).
64
+ #
65
+ # A non-empty converter table costs every request on the route: it makes
66
+ # Mustermann's +identity_params?+ fast path unreachable, so +params+
67
+ # rebuilds the capture Hash through +map_param+ and calls the do-nothing
68
+ # lambda on each value. Passing a Regexp registers no converter at all.
69
+ #
70
+ # The generated matcher differs in one respect: Mustermann expands an
71
+ # Array entry the way it expands a path literal, so each character also
72
+ # matches its own percent-encoding (+v1+ as <tt>(?:v|%76)(?:1|%31)</tt>).
73
+ # A Regexp is inserted verbatim, so a percent-encoded version segment no
74
+ # longer matches the route. It never reached the endpoint anyway --
75
+ # {Versioner::Base#potential_version_match?} compares the raw segment
76
+ # against the declared versions, so +/api/%76%31/x+ was matched here and
77
+ # then rejected as an unknown version, ending in the same cascading 404
78
+ # the router now returns directly.
79
+ #
80
+ # +Regexp.union+ takes Strings and Regexps only, and +version+ accepts
81
+ # Symbols and Integers too, so the entries are coerced first. A lone one
82
+ # would survive without it -- the single-argument path goes through
83
+ # +Regexp.escape+, which does accept a Symbol -- but a second raises
84
+ # TypeError.
85
+ def extract_capture(version, requirements)
86
+ return requirements if version.blank?
46
87
 
47
- PatternCache[[pattern, suffix]]
88
+ requirements.merge(version: Regexp.union(Array.wrap(version).map(&:to_s)))
48
89
  end
49
90
 
50
- def extract_capture(requirements: {}, **options)
51
- requirements = {}.merge(requirements)
52
- DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture|
53
- option = Array(options[field])
54
- capture[field] = option.map(&:to_s) if option.present?
55
- end
91
+ def build_path_from_pattern(pattern, anchor)
92
+ return pattern.dup.insert(pattern.rindex('/') + 1, '?') if pattern.end_with?('*path')
93
+ return pattern if anchor
94
+ return "#{pattern}?*path" if pattern.end_with?('/')
95
+
96
+ "#{pattern}/?*path"
56
97
  end
57
98
 
58
99
  class PatternCache < Grape::Util::Cache
59
100
  def initialize
101
+ super
60
102
  @cache = Hash.new do |h, (pattern, suffix)|
61
103
  h[[pattern, suffix]] = -"#{pattern}#{suffix}"
62
104
  end
@@ -1,90 +1,109 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/pattern'
4
- require 'grape/router/attribute_translator'
5
- require 'forwardable'
6
- require 'pathname'
7
-
8
3
  module Grape
9
4
  class Router
10
- class Route
11
- ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze
12
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
13
- FIXED_NAMED_CAPTURES = %w[format version].freeze
5
+ class Route < BaseRoute
6
+ extend Forwardable
14
7
 
15
- attr_accessor :pattern, :translator, :app, :index, :options
8
+ FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9
+ NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
16
10
 
17
- alias attributes translator
11
+ attr_reader :app, :request_method, :index
18
12
 
19
- extend Forwardable
20
- def_delegators :pattern, :path, :origin
21
- delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
22
-
23
- def method_missing(method_id, *arguments)
24
- match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
25
- if match
26
- method_name = match.captures.last.to_sym
27
- warn_route_methods(method_name, caller(1).shift)
28
- @options[method_name]
29
- else
30
- super
31
- end
32
- end
13
+ def_delegators :@app, :call
33
14
 
34
- def respond_to_missing?(method_id, _)
35
- ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
15
+ def initialize(endpoint, method, pattern, options, forward_match:, params: {}, **route_attributes)
16
+ super(pattern, options, **route_attributes)
17
+ @app = endpoint
18
+ @request_method = upcase_method(method)
19
+ @match_function = forward_match ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
20
+ @declared_params = params
36
21
  end
37
22
 
38
- def route_method
39
- warn_route_methods(:method, caller(1).shift, :request_method)
40
- request_method
23
+ def to_head
24
+ head = dup
25
+ head.convert_to_head_request!
26
+ head
41
27
  end
42
28
 
43
- def route_path
44
- warn_route_methods(:path, caller(1).shift)
45
- pattern.path
29
+ def apply(app)
30
+ @app = app
31
+ self
46
32
  end
47
33
 
48
- def initialize(method, pattern, **options)
49
- method_s = method.to_s
50
- method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
34
+ def match?(input)
35
+ return false if input.blank?
51
36
 
52
- @options = options.merge(method: method_upcase)
53
- @pattern = Pattern.new(pattern, **options)
54
- @translator = AttributeTranslator.new(**options, request_method: method_upcase)
37
+ @match_function.call(input, pattern)
55
38
  end
56
39
 
57
- def exec(env)
58
- @app.call(env)
40
+ # The route's declared params keyed by name — path captures plus any
41
+ # declared body/query params, as their definitions. Used for documentation
42
+ # (e.g. grape-swagger), not for extracting request values.
43
+ def params
44
+ @params ||= pattern.captures_default.merge(@declared_params)
59
45
  end
60
46
 
61
- def apply(app)
62
- @app = app
63
- self
47
+ # Extract param values from a matched request path. Used by the router.
48
+ #
49
+ # A pattern with no named captures has nothing to extract, so it skips the
50
+ # match entirely and answers nil — the same way {GreedyRoute#params_for}
51
+ # does, and what the router already coerces into the Hash it builds
52
+ # routing args in.
53
+ def params_for(input)
54
+ return unless pattern.captures?
55
+
56
+ parsed = pattern.params(input)
57
+ return unless parsed
58
+
59
+ params = {}
60
+ parsed.each { |name, value| params[name.to_sym] = tag_utf8!(value) unless value.nil? }
61
+ params
64
62
  end
65
63
 
66
- def match?(input)
67
- translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input)
64
+ protected
65
+
66
+ def convert_to_head_request!
67
+ @request_method = Rack::HEAD
68
68
  end
69
69
 
70
- def params(input = nil)
71
- if input.nil?
72
- pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults|
73
- defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key)
74
- end
70
+ private
71
+
72
+ # Mustermann decodes path captures out of +PATH_INFO+, which Rack hands us
73
+ # tagged ASCII-8BIT, so path params came back binary while Rack tags query
74
+ # and body params UTF-8. That split makes an API's own declarations
75
+ # disagree with themselves: `values: ['café']` matched `?id=café` but not
76
+ # `/café`, since a binary string never equals the UTF-8 literal it was
77
+ # written as.
78
+ #
79
+ # Re-tag as UTF-8. Nothing obliges a client to send UTF-8: the request
80
+ # target is octets to HTTP, and Rack's SPEC has CGI keys carry non-ASCII
81
+ # as ASCII-8BIT. But UTF-8 is what browsers percent-encode with, what an
82
+ # IRI maps to, and what Rails settles on — ActionDispatch::Journey::Router
83
+ # force_encodes every path capture to UTF-8 after unescaping it.
84
+ #
85
+ # Only the encoding changes; the bytes are untouched. Octets that are not
86
+ # UTF-8 therefore stay invalid and are caught downstream rather than being
87
+ # silently scrubbed into something the client never sent.
88
+ #
89
+ # The re-tag is in place, hence the bang. Mustermann's +Pattern#params+
90
+ # builds a fresh Hash of fresh, unfrozen strings on every call and skips
91
+ # its own Match cache, so the mutation cannot escape this request — which
92
+ # is also what lets the Array branch re-tag its elements with +each+.
93
+ def tag_utf8!(value)
94
+ case value
95
+ when String
96
+ value.force_encoding(Encoding::UTF_8)
97
+ when Array
98
+ value.each { |v| tag_utf8!(v) }
75
99
  else
76
- parsed = pattern.params(input)
77
- parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
100
+ value
78
101
  end
79
102
  end
80
103
 
81
- private
82
-
83
- def warn_route_methods(name, location, expected = nil)
84
- path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
- path = File.realpath(path) if Pathname.new(path).relative?
86
- expected ||= name
87
- ActiveSupport::Deprecation.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.")
104
+ def upcase_method(method)
105
+ method_s = method.to_s
106
+ Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
88
107
  end
89
108
  end
90
109
  end
data/lib/grape/router.rb CHANGED
@@ -1,60 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/route'
4
- require 'grape/util/cache'
5
-
6
3
  module Grape
7
4
  class Router
8
- attr_reader :map, :compiled
9
-
10
- def self.normalize_path(path)
11
- path = +"/#{path}"
12
- path.squeeze!('/')
13
- path.sub!(%r{/+\Z}, '')
14
- path = '/' if path == ''
15
- path
16
- end
17
-
18
- def self.supported_methods
19
- @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
20
- end
21
-
22
5
  def initialize
23
6
  @neutral_map = []
24
7
  @neutral_regexes = []
25
- @map = Hash.new { |hash, key| hash[key] = [] }
26
- @optimized_map = Hash.new { |hash, key| hash[key] = // }
8
+ # Plain hashes with no auto-vivifying default: a lookup for an HTTP method
9
+ # that has no routes must not insert a key. `compile!` freezes both maps,
10
+ # so request-time reads never mutate shared state (see #match? / #rotation).
11
+ @map = {}
12
+ @optimized_map = {}
27
13
  end
28
14
 
29
15
  def compile!
30
- return if compiled
16
+ return if @compiled
31
17
 
32
- @union = Regexp.union(@neutral_regexes)
18
+ @union = resolve_capture_groups(Regexp.union(@neutral_regexes), @neutral_map)
33
19
  @neutral_regexes = nil
34
- self.class.supported_methods.each do |method|
35
- routes = map[method]
36
- @optimized_map[method] = routes.map.with_index do |route, index|
37
- route.index = index
38
- Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
39
- end
40
- @optimized_map[method] = Regexp.union(@optimized_map[method])
20
+ # Compiled from the routes actually registered rather than from
21
+ # Grape::HTTP_SUPPORTED_METHODS. A route declared with any other verb
22
+ # (`route :purge, '/cache'`) is accepted at definition time and is
23
+ # already collected into the resource's Allow header, so skipping it here
24
+ # left it in @map but out of @optimized_map — the only map #match? reads.
25
+ # The resource then advertised a method that could never be matched and
26
+ # answered every request for it with 405.
27
+ @map.each do |method, routes|
28
+ optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
29
+ @optimized_map[method] = resolve_capture_groups(Regexp.union(optimized_map), routes)
41
30
  end
31
+ @map.freeze
32
+ @optimized_map.freeze
42
33
  @compiled = true
43
34
  end
44
35
 
45
36
  def append(route)
46
- map[route.request_method] << route
37
+ (@map[route.request_method] ||= []) << route
47
38
  end
48
39
 
49
- def associate_routes(pattern, **options)
50
- @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
51
- @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
40
+ def associate_routes(greedy_route)
41
+ @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
42
+ @neutral_map << greedy_route
52
43
  end
53
44
 
54
45
  def call(env)
55
46
  with_optimization do
56
- response, route = identity(env)
57
- response || rotation(env, route)
47
+ # Published on the env so the middleware the matched route runs -- the
48
+ # path versioner above all -- reads the path this routed on instead of
49
+ # normalizing PATH_INFO a second time.
50
+ input = env[Grape::Env::GRAPE_NORMALIZED_PATH] = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
51
+ transaction(input, env[Rack::REQUEST_METHOD], env)
58
52
  end
59
53
  end
60
54
 
@@ -65,113 +59,143 @@ module Grape
65
59
  any.endpoint
66
60
  end
67
61
 
62
+ DEFAULT_RESPONSE_HEADERS = Grape::Util::Header.new.merge('X-Cascade' => 'pass').freeze
63
+ DEFAULT_RESPONSE_BODY = ['404 Not Found'].freeze
64
+
68
65
  private
69
66
 
70
- def identity(env)
71
- route = nil
72
- response = transaction(env) do |input, method|
73
- route = match?(input, method)
74
- process_route(route, env) if route
67
+ # Resolve +input+ against the compiled routes, in priority order:
68
+ #
69
+ # 1. the routes registered for +method+ — the compiled-union match first,
70
+ # then, when that route cascades, its siblings (see #rotation);
71
+ # 2. the ANY (+'*'+) routes;
72
+ # 3. the greedy neighbour, which answers auto-OPTIONS and 405.
73
+ #
74
+ # Returns nil when nothing answered, leaving the caller to 404. A response
75
+ # that cascades is never final: it is returned only once every later
76
+ # candidate has declined too, so the caller (or a mounting app upstream)
77
+ # can keep looking.
78
+ def transaction(input, method, env)
79
+ exact_route = match?(input, method)
80
+ response = process_route(exact_route, input, env) if exact_route
81
+ return response if halt?(response)
82
+
83
+ # A cascading route has only declined this request. Its siblings — the
84
+ # routes sharing this path but differing in, say, version — must be
85
+ # tried before falling back to the ANY routes and the greedy neighbour.
86
+ # Skipped when nothing matched: the compiled union is the disjunction of
87
+ # the same patterns #rotation walks, so a miss there is a miss here.
88
+ cascaded = !response.nil?
89
+ if cascaded
90
+ response = rotation(input, method, env, exact_route)
91
+ return response if response && !cascade?(response)
75
92
  end
76
- [response, route]
77
- end
78
93
 
79
- def rotation(env, exact_route = nil)
80
- response = nil
81
- input, method = *extract_input_and_method(env)
82
- map[method].each do |route|
83
- next if exact_route == route
84
- next unless route.match?(input)
94
+ last_neighbor_route = greedy_match?(input)
85
95
 
86
- response = process_route(route, env)
87
- break unless cascade?(response)
88
- end
89
- response
90
- end
96
+ # If last_neighbor_route exists and request method is OPTIONS,
97
+ # return response by using #include_allow_header.
98
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && method == Rack::OPTIONS && last_neighbor_route
91
99
 
92
- def transaction(env)
93
- input, method = *extract_input_and_method(env)
94
- response = yield(input, method)
100
+ star_route = match?(input, '*')
95
101
 
96
- return response if response && !(cascade = cascade?(response))
102
+ if star_route
103
+ close_body(response) if response # superseded by the ANY route
104
+ response = process_route(star_route, input, env)
105
+ return response if halt?(response)
97
106
 
98
- last_neighbor_route = greedy_match?(input)
107
+ cascaded ||= !response.nil?
108
+ end
99
109
 
100
- # If last_neighbor_route exists and request method is OPTIONS,
101
- # return response by using #call_with_allow_headers.
102
- return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
110
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && last_neighbor_route
103
111
 
104
- route = match?(input, '*')
112
+ response
113
+ end
105
114
 
106
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
115
+ # The routes registered for +method+ other than +exact_route+, tried in
116
+ # registration order until one answers without cascading. Returns the last
117
+ # response processed — a cascading one when every sibling declined, so the
118
+ # caller can hand it back — or nil when no sibling matched.
119
+ def rotation(input, method, env, exact_route)
120
+ response = nil
121
+ @map[method]&.each do |route|
122
+ next if exact_route == route
123
+ next unless route.match?(input)
107
124
 
108
- if route
109
- response = process_route(route, env)
110
- return response if response && !(cascade = cascade?(response))
125
+ close_body(response) if response # the previous sibling cascaded
126
+ response = process_route(route, input, env)
127
+ break unless cascade?(response)
111
128
  end
129
+ response
130
+ end
112
131
 
113
- return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
132
+ # Returns true if `response` should be returned as-is from the enclosing
133
+ # transaction. Closes the body as a side effect when the response is
134
+ # cascading so callers can safely try the next match.
135
+ def halt?(response)
136
+ return false unless response
114
137
 
115
- nil
138
+ cascade = cascade?(response)
139
+ close_body(response) if cascade
140
+ !cascade
116
141
  end
117
142
 
118
- def process_route(route, env)
119
- prepare_env_from_route(env, route)
120
- route.exec(env)
143
+ # Releases a response the router has decided not to return. Rack requires
144
+ # every body it hands out to be closed, and a cascading candidate is
145
+ # discarded as soon as a later one answers.
146
+ def close_body(response)
147
+ body = response[2]
148
+ body.close if body.respond_to?(:close)
121
149
  end
122
150
 
123
- def make_routing_args(default_args, route, input)
124
- args = default_args || { route_info: route }
125
- args.merge(route.params(input) || {})
151
+ # Routing args are rebuilt for every attempt: when a route cascades
152
+ # (X-Cascade pass), the next candidate must not observe the previous
153
+ # attempt's +route_info+ or path captures.
154
+ def process_route(route, input, env, include_allow_header: false)
155
+ # The path captures are the hash: +route_info+ is written into them
156
+ # rather than merged in from a second one.
157
+ routing_args = route.params_for(input) || {}
158
+ routing_args[:route_info] = route
159
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
160
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
161
+ route.call(env)
126
162
  end
127
163
 
128
- def extract_input_and_method(env)
129
- input = string_for(env[Grape::Http::Headers::PATH_INFO])
130
- method = env[Grape::Http::Headers::REQUEST_METHOD]
131
- [input, method]
164
+ # Tells each route the number of the group it ended up as in +union+. The
165
+ # numbering is a property of the union rather than of the route's own
166
+ # pattern -- every route ahead of it contributes however many groups its
167
+ # pattern declares -- so it can only be resolved once the union is built.
168
+ # Returns the union, so a caller can assign it in one expression.
169
+ def resolve_capture_groups(union, routes)
170
+ named_captures = union.named_captures
171
+ routes.each { |route| route.resolve_capture_group!(named_captures) }
172
+ union
132
173
  end
133
174
 
134
175
  def with_optimization
135
- compile! unless compiled
176
+ compile!
136
177
  yield || default_response
137
178
  end
138
179
 
139
180
  def default_response
140
- [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
181
+ [404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
141
182
  end
142
183
 
184
+ # Which alternative of the union matched is answered by scanning one group
185
+ # per registered route, so on an API with many of them that scan is what a
186
+ # request costs. The groups are indexed by number rather than by name: a
187
+ # name sends MatchData through the pattern's name table on every lookup,
188
+ # a number indexes the match region directly.
143
189
  def match?(input, method)
144
- current_regexp = @optimized_map[method]
145
- return unless current_regexp.match(input)
146
-
147
- last_match = Regexp.last_match
148
- @map[method].detect { |route| last_match["_#{route.index}"] }
190
+ @optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_group] } }
149
191
  end
150
192
 
151
193
  def greedy_match?(input)
152
- return unless @union.match(input)
153
-
154
- last_match = Regexp.last_match
155
- @neutral_map.detect { |route| last_match["_#{route.index}"] }
156
- end
157
-
158
- def call_with_allow_headers(env, route)
159
- prepare_env_from_route(env, route)
160
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
161
- route.endpoint.call(env)
162
- end
163
-
164
- def prepare_env_from_route(env, route)
165
- input, = *extract_input_and_method(env)
166
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
194
+ @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_group] } }
167
195
  end
168
196
 
169
197
  def cascade?(response)
170
- response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
171
- end
172
-
173
- def string_for(input)
174
- self.class.normalize_path(input)
198
+ response && response[1]['X-Cascade'] == 'pass'
175
199
  end
176
200
  end
177
201
  end
@@ -31,6 +31,13 @@ module Grape
31
31
  def ==(other)
32
32
  path == other.path
33
33
  end
34
+ alias eql? ==
35
+
36
+ # Mirrors #==, which keys on the path alone and does not check the
37
+ # class, so the class must stay out of the hash too.
38
+ def hash
39
+ path.hash
40
+ end
34
41
  end
35
42
  end
36
43
  end
@@ -6,11 +6,9 @@ module Grape
6
6
  # for using Rack::SendFile middleware
7
7
  class SendfileResponse < Rack::Response
8
8
  def respond_to?(method_name, include_all = false)
9
- if method_name == :to_path
10
- @body.respond_to?(:to_path, include_all)
11
- else
12
- super
13
- end
9
+ return @body.respond_to?(:to_path, include_all) if method_name == :to_path
10
+
11
+ super
14
12
  end
15
13
 
16
14
  def to_path
@@ -18,6 +18,13 @@ module Grape
18
18
  def ==(other)
19
19
  stream == other.stream
20
20
  end
21
+ alias eql? ==
22
+
23
+ # Mirrors #==, which keys on the stream alone and does not check the
24
+ # class, so the class must stay out of the hash too.
25
+ def hash
26
+ stream.hash
27
+ end
21
28
  end
22
29
  end
23
30
  end