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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ # Abstract parent for lazy wrappers used by the remount/configuration
7
+ # machinery. Call sites can type-check with +is_a?(Grape::Util::Lazy::Base)+
8
+ # instead of enumerating the concrete subclasses.
9
+ class Base
10
+ def to_s
11
+ evaluate.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Block < Base
7
+ def initialize(&new_block)
8
+ super()
9
+ @block = new_block
10
+ end
11
+
12
+ def evaluate_from(configuration)
13
+ @block.call(configuration)
14
+ end
15
+
16
+ def evaluate
17
+ @block.call({})
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Value < Base
7
+ attr_reader :access_keys
8
+
9
+ def initialize(value, access_keys = [])
10
+ super()
11
+ @value = value
12
+ @access_keys = access_keys
13
+ end
14
+
15
+ def evaluate_from(configuration)
16
+ matching_lazy_value = configuration.fetch(@access_keys)
17
+ matching_lazy_value.evaluate
18
+ end
19
+
20
+ def evaluate
21
+ @value
22
+ end
23
+
24
+ def reached_by(parent_access_keys, access_key)
25
+ @access_keys = parent_access_keys + [access_key]
26
+ self
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueArray < ValueEnumerable
7
+ def initialize(array)
8
+ super
9
+ @value_hash = []
10
+ array.each_with_index do |value, index|
11
+ self[index] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.map(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueEnumerable < Value
7
+ def [](key)
8
+ return Value.new(nil).reached_by(access_keys, key) if @value_hash[key].nil?
9
+
10
+ @value_hash[key].reached_by(access_keys, key)
11
+ end
12
+
13
+ def fetch(access_keys)
14
+ access_keys.reduce(self) { |node, key| node[key] }
15
+ end
16
+
17
+ def []=(key, value)
18
+ value_class = case value
19
+ when Hash
20
+ ValueHash
21
+ when Array
22
+ ValueArray
23
+ else
24
+ Value
25
+ end
26
+ @value_hash[key] = value_class.new(value)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueHash < ValueEnumerable
7
+ def initialize(hash)
8
+ super
9
+ @value_hash = ActiveSupport::HashWithIndifferentAccess.new
10
+ hash.each do |key, value|
11
+ self[key] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.transform_values(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ class MediaType
6
+ attr_reader :type, :subtype, :vendor, :version, :format
7
+
8
+ # based on the HTTP Accept header with the pattern:
9
+ # application/vnd.:vendor-:version+:format
10
+ #
11
+ # Matched against a down-cased media type: they are case-insensitive
12
+ # (RFC 9110 §8.3.1), while a vendor and version are declared in the DSL
13
+ # in the case they will be compared in.
14
+ VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/
15
+
16
+ def initialize(type:, subtype:)
17
+ @type = type
18
+ @subtype = subtype
19
+ VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m|
20
+ @vendor = m[:vendor]
21
+ @version = m[:version]
22
+ @format = m[:format]
23
+ end
24
+ end
25
+
26
+ def ==(other)
27
+ self.class == other.class &&
28
+ other.type == type &&
29
+ other.subtype == subtype &&
30
+ other.vendor == vendor &&
31
+ other.version == version &&
32
+ other.format == format
33
+ end
34
+ alias eql? ==
35
+
36
+ def hash
37
+ [self.class, type, subtype, vendor, version, format].hash
38
+ end
39
+
40
+ class << self
41
+ def best_quality(header, available_media_types)
42
+ parse(best_quality_media_type(header, available_media_types))
43
+ end
44
+
45
+ def parse(media_type)
46
+ return if media_type.blank?
47
+
48
+ type, subtype = media_type.downcase.split('/', 2)
49
+ return if type.blank? || subtype.blank?
50
+
51
+ new(type:, subtype:)
52
+ end
53
+
54
+ def match?(media_type)
55
+ return false if media_type.blank?
56
+
57
+ subtype = media_type.downcase.split('/', 2).last
58
+ return false if subtype.blank?
59
+
60
+ VENDOR_VERSION_HEADER_REGEX.match?(subtype)
61
+ end
62
+
63
+ # The available types are registered in lower case and Rack matches them
64
+ # literally, so the header has to be down-cased to be compared against
65
+ # them at all.
66
+ def best_quality_media_type(header, available_media_types)
67
+ header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header.downcase, available_media_types)
68
+ end
69
+ end
70
+
71
+ private_class_method :best_quality_media_type
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module PathNormalizer
6
+ # Taken from Rails
7
+ # call("/foo") # => "/foo"
8
+ # call("/foo/") # => "/foo"
9
+ # call("foo") # => "/foo"
10
+ # call("") # => "/"
11
+ # call("/%ab") # => "/%AB"
12
+ # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
13
+ def self.call(path)
14
+ return '/' unless path
15
+ return path if path == '/'
16
+
17
+ # Fast path for the overwhelming majority of paths that don't need to be
18
+ # normalized. Two String#include? calls rather than one `%|//` regexp:
19
+ # same predicate, and the scan stays in C without building a match.
20
+ return path if path.start_with?('/') && !(path.end_with?('/') || path.include?('%') || path.include?('//'))
21
+
22
+ # Slow path. The bangs below are safe because +normalized+ is a fresh
23
+ # String built here, never the one the caller passed in.
24
+ encoding = path.encoding
25
+ normalized = "/#{path}"
26
+ normalized.squeeze!('/')
27
+
28
+ unless normalized == '/'
29
+ normalized.delete_suffix!('/')
30
+ normalized.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
31
+ end
32
+
33
+ normalized.force_encoding(encoding)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Registry
6
+ def register(klass)
7
+ short_name = build_short_name(klass)
8
+ return if short_name.nil?
9
+
10
+ warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name)
11
+ registry[short_name] = registry[short_name.to_sym] = klass
12
+ end
13
+
14
+ private
15
+
16
+ def build_short_name(klass)
17
+ return if klass.name.blank?
18
+
19
+ klass.name.demodulize.underscore
20
+ end
21
+
22
+ # Every registration under both spellings in one plain Hash, so a lookup
23
+ # is a single +Hash#[]+. A +HashWithIndifferentAccess+ converted the key
24
+ # on every read instead -- and these registries are read on the request
25
+ # path, by +Grape::Formatter+ once per response, by +Grape::Parser+ once
26
+ # per parsed body and by +Grape::ParamsBuilder+ once per params build,
27
+ # always with a Symbol, which is the spelling +convert_key+ allocates a
28
+ # String for.
29
+ #
30
+ # +register+ derives the short name as a String, so the Symbol is the
31
+ # alias.
32
+ def registry
33
+ @registry ||= {}
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ # Diagnostics for +rescue_from+ registrations that can never run.
6
+ #
7
+ # Middleware::Error resolves with +find+, so within a scope the first
8
+ # matching class wins and one registered for a class an earlier handler
9
+ # already covers is dead code — silently, before this warned:
10
+ #
11
+ # rescue_from StandardError do ... end # wins
12
+ # rescue_from ArgumentError do ... end # never runs
13
+ #
14
+ # Warn rather than reorder: which should win is the author's call, and
15
+ # +rescue_from :all+ (consulted only after the registered handlers) already
16
+ # offers "broad first, specific still wins" to anyone who wants it.
17
+ module ShadowedRescueHandlers
18
+ module_function
19
+
20
+ # @param registered [Hash] the scope's own handlers, in registration order
21
+ # @param mapping [Hash] the handlers being registered now
22
+ # @return [void]
23
+ #
24
+ # Only a scope's own registrations are compared: across scopes the nearest
25
+ # one deliberately wins, so an inner +rescue_from StandardError+ shadowing
26
+ # an outer +rescue_from ArgumentError+ is the documented behaviour rather
27
+ # than a mistake. Classes sharing a handler object are skipped too, since
28
+ # +rescue_from A, B+ registers one handler for both and the entry that
29
+ # loses to the other changes nothing.
30
+ def warn_about(registered, mapping)
31
+ return if registered.empty?
32
+
33
+ mapping.each do |klass, handler|
34
+ covered_by, = registered.find { |already, existing| klass <= already && !existing.equal?(handler) }
35
+ next unless covered_by
36
+
37
+ warn(message_for(klass, covered_by))
38
+ end
39
+ end
40
+
41
+ def message_for(klass, covered_by)
42
+ return "Grape: rescue_from #{klass} was already registered in this scope; the first handler is kept and this one will never run." if klass == covered_by
43
+
44
+ "Grape: rescue_from #{klass} will never run — #{covered_by} was registered earlier in the same scope " \
45
+ 'and is matched first. Register the more specific class before the broader one.'
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,28 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base_inheritable'
4
-
5
3
  module Grape
6
4
  module Util
7
- class StackableValues < BaseInheritable
8
- # Even if there is no value, an empty array will be returned.
5
+ # A read-only view of one settings scope's stackable registrations.
6
+ #
7
+ # Grape stores nothing here anymore: the registrations live on
8
+ # Grape::Util::InheritableSetting, one Array per key per scope, and are
9
+ # reached through its semantic accessors (+helpers+, +middleware+,
10
+ # +namespaces+, ...). This class survives only because grape-swagger
11
+ # reads InheritableSetting#namespace_stackable directly — including
12
+ # walking the #inherited_values chain and reading each level's own
13
+ # #new_values to recover per-scope registrations — and is expected to be
14
+ # removed once grape-swagger reads those accessors instead.
15
+ #
16
+ # Instances are built on demand by InheritableSetting#namespace_stackable
17
+ # and are not the backing store: writing to #new_values does not register
18
+ # anything.
19
+ class StackableValues
20
+ EMPTY = [].freeze
21
+
22
+ attr_reader :inherited_values, :new_values
23
+
24
+ # @param new_values [Hash, nil] the scope's own registrations, one Array
25
+ # per key; nil when the scope never registered anything.
26
+ # @param inherited_values [StackableValues, Hash] the enclosing scope's
27
+ # view, or an empty Hash at the root of the chain.
28
+ def initialize(new_values, inherited_values)
29
+ @new_values = new_values
30
+ @inherited_values = inherited_values
31
+ end
32
+
33
+ # Outermost scope first. Even if there is no value, an empty (frozen)
34
+ # array will be returned.
9
35
  def [](name)
10
- inherited_value = inherited_values[name]
11
- new_value = new_values[name]
36
+ inherited_value = @inherited_values[name]
37
+ new_value = @new_values && @new_values[name]
12
38
 
13
- return new_value || [] unless inherited_value
39
+ return new_value || EMPTY unless inherited_value
14
40
 
15
41
  concat_values(inherited_value, new_value)
16
42
  end
17
43
 
18
- def []=(name, value)
19
- new_values[name] ||= []
20
- new_values[name].push value
44
+ def keys
45
+ return @inherited_values.keys if @new_values.nil? || @new_values.empty?
46
+
47
+ (@inherited_values.keys + @new_values.keys).uniq
21
48
  end
22
49
 
23
50
  def to_hash
24
- keys.each_with_object({}) do |key, result|
25
- result[key] = self[key]
51
+ keys.to_h do |key|
52
+ [key, self[key]]
26
53
  end
27
54
  end
28
55
 
@@ -31,10 +58,7 @@ module Grape
31
58
  def concat_values(inherited_value, new_value)
32
59
  return inherited_value unless new_value
33
60
 
34
- [].tap do |value|
35
- value.concat(inherited_value)
36
- value.concat(new_value)
37
- end
61
+ inherited_value + new_value
38
62
  end
39
63
  end
40
64
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Translation
6
+ FALLBACK_LOCALE = :en
7
+ private_constant :FALLBACK_LOCALE
8
+ # Sentinel returned by I18n when a key is missing (passed as the default:
9
+ # value). Using a named class rather than plain Object.new makes it
10
+ # identifiable in debug output and immune to backends that call .to_s on
11
+ # the default before returning it.
12
+ MISSING = Class.new { def inspect = 'Grape::Util::Translation::MISSING' }.new.freeze
13
+ private_constant :MISSING
14
+
15
+ private
16
+
17
+ # Extra keyword args (**) are forwarded verbatim to I18n as interpolation
18
+ # variables (e.g. +min:+, +max:+ from LengthValidator's Hash message).
19
+ # Callers must not pass unintended keyword arguments — any extra keyword
20
+ # will silently become an I18n interpolation variable.
21
+ def translate(key, default: MISSING, scope: 'grape.errors.messages', locale: nil, **)
22
+ i18n_opts = { default:, scope:, ** }
23
+ i18n_opts[:locale] = locale if locale
24
+ message = ::I18n.translate(key, **i18n_opts)
25
+ return message unless message.equal?(MISSING)
26
+
27
+ effective_default = default.equal?(MISSING) ? [*Array(scope), key].join('.') : default
28
+ return effective_default if fallback_locale?(locale) || fallback_locale_unavailable?
29
+
30
+ ::I18n.translate(key, default: effective_default, scope:, locale: FALLBACK_LOCALE, **)
31
+ end
32
+
33
+ def fallback_locale?(locale)
34
+ (locale || ::I18n.locale) == FALLBACK_LOCALE
35
+ end
36
+
37
+ def fallback_locale_unavailable?
38
+ ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -3,55 +3,88 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AttributesIterator
6
- include Enumerable
7
-
8
- attr_reader :scope
9
-
10
- def initialize(validator, scope, params)
6
+ # +attrs+ and +scope+ are static per validator; only +params+ varies
7
+ # per request, so an instance can be built once and reused (it keeps
8
+ # no request-derived state). Reused instances are shared across
9
+ # threads, so +each+ must stay free of mutable instance state.
10
+ def initialize(attrs, scope)
11
+ @attrs = attrs
11
12
  @scope = scope
12
- @attrs = validator.attrs
13
- @original_params = scope.params(params)
14
- @params = Array.wrap(@original_params)
13
+ # How many times #do_each may descend into a nested array. The
14
+ # declaration allows one level per Array-typed scope on the chain, less
15
+ # the one +Array.wrap+ already consumes in #each. Anything deeper was
16
+ # put there by the request, not by the declaration.
17
+ @max_nesting = [scope.array_depth - 1, 0].max
15
18
  end
16
19
 
17
- def each(&block)
18
- do_each(@params, &block) # because we need recursion for nested arrays
20
+ def each(params, &)
21
+ original_params = @scope.params(params)
22
+ # A scope resolves to a Hash unless the declaration nests arrays, and
23
+ # then #do_each has nothing to do but hand it straight back: Array.wrap
24
+ # boxes it, the loop unboxes it on its only iteration, and with no Array
25
+ # anywhere neither the nesting descent nor the index bookkeeping
26
+ # applies. Every validator on a flat +params+ block comes through here.
27
+ return yield_attributes(original_params, &) if original_params.is_a?(Hash) && !@scope.iterates_elements?
28
+
29
+ # because we need recursion for nested arrays
30
+ do_each(Array.wrap(original_params), original_params, &)
19
31
  end
20
32
 
21
33
  private
22
34
 
23
- def do_each(params_to_process, parent_indicies = [], &block)
35
+ def do_each(params_to_process, original_params, parent_indices = [], &block)
24
36
  params_to_process.each_with_index do |resource_params, index|
25
37
  # when we get arrays of arrays it means that target element located inside array
26
- # we need this because we want to know parent arrays indicies
27
- if resource_params.is_a?(Array)
28
- do_each(resource_params, [index] + parent_indicies, &block)
38
+ # we need this because we want to know parent arrays indices
39
+ #
40
+ # Only descend as far as the declaration nests. A request that wraps
41
+ # its elements deeper than that is yielded as-is, so the attribute
42
+ # validators see a non-hash and fail it the same way any other
43
+ # unexpected element type does.
44
+ if resource_params.is_a?(Array) && parent_indices.size < @max_nesting
45
+ do_each(resource_params, original_params, [index] + parent_indices, &block)
29
46
  next
30
47
  end
31
48
 
32
- if @scope.type == Array
33
- next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array
49
+ if @scope.iterates_elements?
50
+ next unless original_params.is_a?(Array) # do not validate content of array if it isn't array
34
51
 
35
- # fill current and parent scopes with correct array indicies
36
- parent_scope = @scope.parent
37
- parent_indicies.each do |parent_index|
38
- parent_scope.index = parent_index
39
- parent_scope = parent_scope.parent
40
- end
41
- @scope.index = index
52
+ store_indices(@scope, index, parent_indices)
53
+ elsif original_params.is_a?(Array)
54
+ # Lateral scope (no @element) whose params resolved to an array —
55
+ # delegate index tracking to the nearest element-iterating ancestor
56
+ # so that full_name produces the correct bracketed index.
57
+ target = @scope.nearest_array_ancestor
58
+ store_indices(target, index, parent_indices) if target
42
59
  end
43
60
 
44
- yield_attributes(resource_params, @attrs, &block)
61
+ yield_attributes(resource_params, &block)
62
+ end
63
+ end
64
+
65
+ def store_indices(target_scope, index, parent_indices)
66
+ # No tracker means we're outside a ParamScopeTracker.track block (e.g.
67
+ # a unit test that invokes a validator directly). Index tracking is
68
+ # skipped — full_name will produce bracket-less names — but validation
69
+ # continues rather than crashing.
70
+ tracker = ParamScopeTracker.current or return
71
+ parent_scope = target_scope.parent
72
+ parent_indices.each do |parent_index|
73
+ break unless parent_scope
74
+
75
+ tracker.store_index(parent_scope, parent_index)
76
+ parent_scope = parent_scope.parent
45
77
  end
78
+ tracker.store_index(target_scope, index)
46
79
  end
47
80
 
48
- def yield_attributes(_resource_params, _attrs)
81
+ def yield_attributes(_resource_params)
49
82
  raise NotImplementedError
50
83
  end
51
84
 
52
- # This is a special case so that we can ignore tree's where option
53
- # values are missing lower down. Unfortunately we can remove this
54
- # are the parameter parsing stage as they are required to ensure
85
+ # This is a special case so that we can ignore trees where option
86
+ # values are missing lower down. Unfortunately we can't remove this
87
+ # at the parameter parsing stage as they are required to ensure
55
88
  # the correct indexing is maintained
56
89
  def skip?(val)
57
90
  val == Grape::DSL::Parameters::EmptyOptionalValue
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Immutable value object describing how a parameter is coerced. Assembled
6
+ # by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/
7
+ # +coerce_message+ declaration — never written by the user — and consumed
8
+ # by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by
9
+ # {Validators::Validators::CoerceValidator} (which receives it as its
10
+ # +options+ argument).
11
+ #
12
+ # All three fields may be +nil+ (e.g. a remountable API evaluated on its
13
+ # base instance has no resolved +type+ yet).
14
+ # +coerce_method+ (not +method+) avoids shadowing +Object#method+.
15
+ CoerceOptions = Data.define(:type, :coerce_method, :message) do
16
+ def initialize(type: nil, coerce_method: nil, message: nil)
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ class ContractScope
6
+ # Declare the contract to be used for the endpoint's parameters.
7
+ # @param api [API] the API endpoint to modify.
8
+ # @param contract the contract or schema to be used for validation. Optional.
9
+ # @yield a block yielding a new schema class. Optional.
10
+ def initialize(api, contract = nil, &block)
11
+ # When block is passed, the first arg is either schema or nil.
12
+ declared = block ? Dry::Schema.Params(parent: contract, &block) : contract
13
+
14
+ if declared.respond_to?(:schema)
15
+ # It's a Dry::Validation::Contract, then.
16
+ schema = declared.new
17
+ key_map = schema.schema.key_map
18
+ else
19
+ # Dry::Schema::Processor, hopefully.
20
+ schema = declared
21
+ key_map = declared.key_map
22
+ end
23
+
24
+ api.inheritable_setting.add_contract_key_map(key_map)
25
+ api.inheritable_setting.add_validation(Validators::ContractScopeValidator.new(schema:))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,7 +5,7 @@ module Grape
5
5
  class MultipleAttributesIterator < AttributesIterator
6
6
  private
7
7
 
8
- def yield_attributes(resource_params, _attrs)
8
+ def yield_attributes(resource_params)
9
9
  yield resource_params unless skip?(resource_params)
10
10
  end
11
11
  end