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
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Router
5
+ # Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` /
6
+ # `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint
7
+ # (driven by Grape's `params` option).
8
+ #
9
+ # Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase
10
+ # and Daniel Doubrovkine. Grape instantiates this class directly (see
11
+ # {Grape::Router::Pattern}), so unlike the gem it is not registered as a
12
+ # Mustermann `type: :grape`.
13
+ class MustermannPattern < ::Mustermann::AST::Pattern
14
+ supported_options :params
15
+
16
+ on(nil, '?', ')') { |c| unexpected(c) }
17
+
18
+ on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
19
+ on(':') do |_c|
20
+ param_name = scan(/\w+/)
21
+ # Integer params (declared via Grape's `params` option) match digits only;
22
+ # any other capture matches a single path segment (anything but / ? # .).
23
+ param_type = pattern&.options&.dig(:params, param_name, :type)
24
+ constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]'
25
+ node(:capture, param_name, constraint:) { scan(/\w+/) }
26
+ end
27
+ on('\\') { |_c| node(:char, expect(/./)) }
28
+ on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) }
29
+ on('|') { |_c| node(:or) }
30
+
31
+ on('{') do |_c|
32
+ type = scan('+') ? :named_splat : :capture
33
+ name = expect(/[\w.]+/)
34
+ type = :splat if (type == :named_splat) && (name == 'splat')
35
+ expect('}')
36
+ node(type, name)
37
+ end
38
+
39
+ suffix('?') do |_c, element|
40
+ node(:optional, element)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,62 +1,58 @@
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
7
+
8
+ DEFAULT_CAPTURES = %w[format version].freeze
12
9
 
13
10
  attr_reader :origin, :path, :pattern, :to_regexp
14
11
 
15
- extend Forwardable
16
- def_delegators :pattern, :named_captures, :params
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
+ def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:)
17
+ @origin = origin
18
+ @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
19
+ @pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
24
20
  @to_regexp = @pattern.to_regexp
25
21
  end
26
22
 
23
+ def captures_default
24
+ to_regexp.names
25
+ .delete_if { |n| DEFAULT_CAPTURES.include?(n) }
26
+ .to_h { |k| [k, ''] }
27
+ end
28
+
27
29
  private
28
30
 
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
34
- end
31
+ def extract_capture(format, version, requirements)
32
+ capture = {}
33
+ capture[:format] = map_str(format) if format.present?
34
+ capture[:version] = map_str(version) if version.present?
35
35
 
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
36
+ return capture if requirements.blank?
42
37
 
43
- pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = "?#{parts.last}"
45
- end.join('/') if pattern.end_with?('*path')
38
+ requirements.merge(capture)
39
+ end
46
40
 
47
- PatternCache[[pattern, suffix]]
41
+ def build_path_from_pattern(pattern, anchor)
42
+ return pattern.dup.insert(pattern.rindex('/') + 1, '?') if pattern.end_with?('*path')
43
+ return pattern if anchor
44
+ return "#{pattern}?*path" if pattern.end_with?('/')
45
+
46
+ "#{pattern}/?*path"
48
47
  end
49
48
 
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
49
+ def map_str(value)
50
+ Array.wrap(value).map(&:to_s)
56
51
  end
57
52
 
58
53
  class PatternCache < Grape::Util::Cache
59
54
  def initialize
55
+ super
60
56
  @cache = Hash.new do |h, (pattern, suffix)|
61
57
  h[[pattern, suffix]] = -"#{pattern}#{suffix}"
62
58
  end
@@ -1,61 +1,26 @@
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
14
-
15
- attr_accessor :pattern, :translator, :app, :index, :options
16
-
17
- alias attributes translator
18
-
5
+ class Route < BaseRoute
19
6
  extend Forwardable
20
- def_delegators :pattern, :path, :origin
21
- delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
22
7
 
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
8
+ FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9
+ NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
33
10
 
34
- def respond_to_missing?(method_id, _)
35
- ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
36
- end
11
+ attr_reader :app, :request_method, :index
37
12
 
38
- def route_method
39
- warn_route_methods(:method, caller(1).shift, :request_method)
40
- request_method
41
- end
13
+ def_delegators :@app, :call
42
14
 
43
- def route_path
44
- warn_route_methods(:path, caller(1).shift)
45
- pattern.path
15
+ def initialize(endpoint, method, pattern, options)
16
+ super(pattern, options)
17
+ @app = endpoint
18
+ @request_method = upcase_method(method)
19
+ @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
46
20
  end
47
21
 
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
51
-
52
- @options = options.merge(method: method_upcase)
53
- @pattern = Pattern.new(pattern, **options)
54
- @translator = AttributeTranslator.new(**options, request_method: method_upcase)
55
- end
56
-
57
- def exec(env)
58
- @app.call(env)
22
+ def convert_to_head_request!
23
+ @request_method = Rack::HEAD
59
24
  end
60
25
 
61
26
  def apply(app)
@@ -64,27 +29,29 @@ module Grape
64
29
  end
65
30
 
66
31
  def match?(input)
67
- translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input)
32
+ return false if input.blank?
33
+
34
+ @match_function.call(input, pattern)
68
35
  end
69
36
 
70
37
  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
75
- else
76
- parsed = pattern.params(input)
77
- parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
78
- end
38
+ return params_without_input if input.blank?
39
+
40
+ parsed = pattern.params(input)
41
+ return unless parsed
42
+
43
+ parsed.compact.symbolize_keys
79
44
  end
80
45
 
81
46
  private
82
47
 
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}.")
48
+ def params_without_input
49
+ @params_without_input ||= pattern.captures_default.merge(options[:params])
50
+ end
51
+
52
+ def upcase_method(method)
53
+ method_s = method.to_s
54
+ Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
88
55
  end
89
56
  end
90
57
  end
data/lib/grape/router.rb CHANGED
@@ -1,60 +1,57 @@
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
-
5
+ # @deprecated Use {Grape::Util::PathNormalizer.call} instead.
10
6
  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 + ['*']
7
+ Grape.deprecator.warn(
8
+ '`Grape::Router.normalize_path` is deprecated. Use `Grape::Util::PathNormalizer.call` instead.'
9
+ )
10
+ Grape::Util::PathNormalizer.call(path)
20
11
  end
21
12
 
22
13
  def initialize
23
14
  @neutral_map = []
24
15
  @neutral_regexes = []
25
- @map = Hash.new { |hash, key| hash[key] = [] }
26
- @optimized_map = Hash.new { |hash, key| hash[key] = // }
16
+ # Plain hashes with no auto-vivifying default: a lookup for an HTTP method
17
+ # that has no routes must not insert a key. `compile!` freezes both maps,
18
+ # so request-time reads never mutate shared state (see #match? / #rotation).
19
+ @map = {}
20
+ @optimized_map = {}
27
21
  end
28
22
 
29
23
  def compile!
30
- return if compiled
24
+ return if @compiled
31
25
 
32
26
  @union = Regexp.union(@neutral_regexes)
33
27
  @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])
28
+ (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method|
29
+ next unless @map.key?(method)
30
+
31
+ routes = @map[method]
32
+ optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
33
+ @optimized_map[method] = Regexp.union(optimized_map)
41
34
  end
35
+ @map.freeze
36
+ @optimized_map.freeze
42
37
  @compiled = true
43
38
  end
44
39
 
45
40
  def append(route)
46
- map[route.request_method] << route
41
+ (@map[route.request_method] ||= []) << route
47
42
  end
48
43
 
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)
44
+ def associate_routes(greedy_route)
45
+ @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
46
+ @neutral_map << greedy_route
52
47
  end
53
48
 
54
49
  def call(env)
55
50
  with_optimization do
56
- response, route = identity(env)
57
- response || rotation(env, route)
51
+ input = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
52
+ method = env[Rack::REQUEST_METHOD]
53
+ response, route = identity(input, method, env)
54
+ response || rotation(input, method, env, route)
58
55
  end
59
56
  end
60
57
 
@@ -65,113 +62,101 @@ module Grape
65
62
  any.endpoint
66
63
  end
67
64
 
65
+ DEFAULT_RESPONSE_HEADERS = Grape::Util::Header.new.merge('X-Cascade' => 'pass').freeze
66
+ DEFAULT_RESPONSE_BODY = ['404 Not Found'].freeze
67
+
68
68
  private
69
69
 
70
- def identity(env)
70
+ def identity(input, method, env)
71
71
  route = nil
72
- response = transaction(env) do |input, method|
72
+ response = transaction(input, method, env) do
73
73
  route = match?(input, method)
74
- process_route(route, env) if route
74
+ process_route(route, input, env) if route
75
75
  end
76
76
  [response, route]
77
77
  end
78
78
 
79
- def rotation(env, exact_route = nil)
79
+ def rotation(input, method, env, exact_route)
80
80
  response = nil
81
- input, method = *extract_input_and_method(env)
82
- map[method].each do |route|
81
+ @map[method]&.each do |route|
83
82
  next if exact_route == route
84
83
  next unless route.match?(input)
85
84
 
86
- response = process_route(route, env)
85
+ response = process_route(route, input, env)
87
86
  break unless cascade?(response)
88
87
  end
89
88
  response
90
89
  end
91
90
 
92
- def transaction(env)
93
- input, method = *extract_input_and_method(env)
94
- response = yield(input, method)
95
-
96
- return response if response && !(cascade = cascade?(response))
91
+ def transaction(input, method, env)
92
+ response = yield
93
+ return response if halt?(response)
97
94
 
95
+ last_response_cascade = !response.nil?
98
96
  last_neighbor_route = greedy_match?(input)
99
97
 
100
98
  # 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
99
+ # return response by using #include_allow_header.
100
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && method == Rack::OPTIONS && last_neighbor_route
103
101
 
104
102
  route = match?(input, '*')
105
103
 
106
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
104
+ return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route
107
105
 
108
106
  if route
109
- response = process_route(route, env)
110
- return response if response && !(cascade = cascade?(response))
107
+ route_response = process_route(route, input, env)
108
+ return route_response if halt?(route_response)
109
+
110
+ last_response_cascade = !route_response.nil?
111
111
  end
112
112
 
113
- return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
113
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && last_neighbor_route
114
114
 
115
115
  nil
116
116
  end
117
117
 
118
- def process_route(route, env)
119
- prepare_env_from_route(env, route)
120
- route.exec(env)
121
- end
118
+ # Returns true if `response` should be returned as-is from the enclosing
119
+ # transaction. Closes the body as a side effect when the response is
120
+ # cascading so callers can safely try the next match.
121
+ def halt?(response)
122
+ return false unless response
122
123
 
123
- def make_routing_args(default_args, route, input)
124
- args = default_args || { route_info: route }
125
- args.merge(route.params(input) || {})
124
+ cascade = cascade?(response)
125
+ response[2].close if cascade && response[2].respond_to?(:close)
126
+ !cascade
126
127
  end
127
128
 
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]
129
+ # Routing args are rebuilt for every attempt: when a route cascades
130
+ # (X-Cascade pass), the next candidate must not observe the previous
131
+ # attempt's +route_info+ or path captures.
132
+ def process_route(route, input, env, include_allow_header: false)
133
+ route_params = route.params(input)
134
+ routing_args = { route_info: route }
135
+ routing_args.merge!(route_params) if route_params.present?
136
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
137
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
138
+ route.call(env)
132
139
  end
133
140
 
134
141
  def with_optimization
135
- compile! unless compiled
142
+ compile!
136
143
  yield || default_response
137
144
  end
138
145
 
139
146
  def default_response
140
- [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
147
+ [404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
141
148
  end
142
149
 
143
150
  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}"] }
151
+ @optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } }
149
152
  end
150
153
 
151
154
  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)
155
+ @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } }
167
156
  end
168
157
 
169
158
  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)
159
+ response && response[1]['X-Cascade'] == 'pass'
175
160
  end
176
161
  end
177
162
  end
@@ -31,6 +31,7 @@ module Grape
31
31
  def ==(other)
32
32
  path == other.path
33
33
  end
34
+ alias eql? ==
34
35
  end
35
36
  end
36
37
  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,7 @@ module Grape
18
18
  def ==(other)
19
19
  stream == other.stream
20
20
  end
21
+ alias eql? ==
21
22
  end
22
23
  end
23
24
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Testing
5
+ module RunBeforeEach
6
+ def run
7
+ self.class.run_before_each(self)
8
+ super
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def before_each(&block)
14
+ raise ArgumentError, 'a block is required' unless block
15
+
16
+ @before_each ||= []
17
+ @before_each << block
18
+ end
19
+
20
+ def reset_before_each
21
+ @before_each&.clear
22
+ end
23
+
24
+ def run_before_each(endpoint)
25
+ superclass.run_before_each(endpoint) unless self == Grape::Endpoint
26
+ @before_each&.each { |blk| blk.call(endpoint) }
27
+ end
28
+ end
29
+
30
+ Grape::Endpoint.prepend(RunBeforeEach)
31
+ Grape::Endpoint.extend(ClassMethods)
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ class ApiDescription
6
+ DSL_METHODS = %i[
7
+ body_name
8
+ consumes
9
+ default
10
+ deprecated
11
+ detail
12
+ entity
13
+ headers
14
+ hidden
15
+ http_codes
16
+ is_array
17
+ named
18
+ nickname
19
+ params
20
+ produces
21
+ security
22
+ summary
23
+ tags
24
+ ].freeze
25
+
26
+ def initialize(description, endpoint_configuration, &)
27
+ @endpoint_configuration = endpoint_configuration
28
+ @attributes = { description: }
29
+ instance_eval(&)
30
+ end
31
+
32
+ DSL_METHODS.each do |attribute|
33
+ define_method attribute do |value|
34
+ @attributes[attribute] = value
35
+ end
36
+ end
37
+
38
+ alias success entity
39
+ alias failure http_codes
40
+
41
+ def configuration
42
+ @configuration ||= eval_endpoint_config(@endpoint_configuration)
43
+ end
44
+
45
+ def settings
46
+ @attributes
47
+ end
48
+
49
+ private
50
+
51
+ def eval_endpoint_config(configuration)
52
+ return configuration if configuration.is_a?(Hash)
53
+
54
+ configuration.evaluate
55
+ end
56
+ end
57
+ end
58
+ end