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,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/util/registrable'
4
-
5
3
  module Grape
6
4
  module ContentTypes
7
- extend Util::Registrable
5
+ module_function
8
6
 
9
7
  # Content types are listed in order of preference.
10
- CONTENT_TYPES = {
8
+ DEFAULTS = {
11
9
  xml: 'application/xml',
12
10
  serializable_hash: 'application/json',
13
11
  json: 'application/json',
@@ -15,13 +13,73 @@ module Grape
15
13
  txt: 'text/plain'
16
14
  }.freeze
17
15
 
18
- class << self
19
- def content_types_for_settings(settings)
20
- settings&.inject(:merge!)
16
+ MIME_TYPES = Grape::ContentTypes::DEFAULTS.except(:serializable_hash).invert.freeze
17
+
18
+ def content_types_for(from_settings)
19
+ from_settings.presence || DEFAULTS
20
+ end
21
+
22
+ def mime_types_for(from_settings)
23
+ return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS
24
+
25
+ MimeTypesCache[from_settings]
26
+ end
27
+
28
+ # Every format under both spellings in one plain Hash, so a lookup is a
29
+ # single +Hash#[]+. +HashWithIndifferentAccess+ converted the key on every
30
+ # read instead, and this is read two or three times per request — to
31
+ # negotiate the format, and again to set the response content type.
32
+ #
33
+ # Keys arrive as Symbols: the +content_type+ DSL symbolizes what it is
34
+ # given and the defaults are Symbols. The key is stored as it came too,
35
+ # so a middleware constructed directly with String keys still answers to
36
+ # either spelling, as the indifferent hash did.
37
+ def lookup_for(from_settings)
38
+ LookupCache[from_settings]
39
+ end
40
+
41
+ # The media type of a content-type header: the part before any `;`
42
+ # parameters, with surrounding whitespace removed
43
+ # (e.g. `'text/html'` for `'text/html; charset=utf-8'`). Returns nil for a
44
+ # nil content type. Skips the split (and its allocation) when there are no
45
+ # parameters, which is the common case.
46
+ def media_type(content_type)
47
+ return if content_type.nil?
48
+
49
+ base = content_type.include?(';') ? content_type.split(';', 2).first : content_type
50
+ base.strip
51
+ end
52
+
53
+ # Both tables below are derived from nothing but the content-type registry,
54
+ # and one content-type-aware middleware is built per API instance — so an
55
+ # app mounting N APIs held N copies of tables it only ever reads. Keying
56
+ # the cache on the registry itself collapses them: Hash keys compare by
57
+ # value, so every API that registers the same content types shares one
58
+ # table.
59
+ #
60
+ # Both the key and the table are frozen: the caller's registry stays
61
+ # reachable (through +middleware.options[:content_types]+, among others)
62
+ # and mutating a live key would corrupt a cache that is now shared
63
+ # process-wide.
64
+
65
+ class MimeTypesCache < Grape::Util::Cache
66
+ def initialize
67
+ super
68
+ @cache = Hash.new do |h, from_settings|
69
+ h[from_settings.dup.freeze] = from_settings.invert.transform_keys! { |mime_type| Grape::ContentTypes.media_type(mime_type) }.freeze
70
+ end
21
71
  end
72
+ end
22
73
 
23
- def content_types_for(from_settings)
24
- from_settings.presence || Grape::ContentTypes::CONTENT_TYPES.merge(default_elements)
74
+ class LookupCache < Grape::Util::Cache
75
+ def initialize
76
+ super
77
+ @cache = Hash.new do |h, from_settings|
78
+ h[from_settings.dup.freeze] = from_settings.each_with_object({}) do |(format, media_type), lookup|
79
+ lookup[format] = media_type
80
+ lookup[format.is_a?(String) ? format.to_sym : format.to_s] = media_type
81
+ end.freeze
82
+ end
25
83
  end
26
84
  end
27
85
  end
data/lib/grape/cookies.rb CHANGED
@@ -2,42 +2,45 @@
2
2
 
3
3
  module Grape
4
4
  class Cookies
5
- def initialize
6
- @cookies = {}
7
- @send_cookies = {}
8
- end
5
+ extend Forwardable
9
6
 
10
- def read(request)
11
- request.cookies.each do |name, value|
12
- @cookies[name.to_s] = value
13
- end
14
- end
7
+ DELETED_COOKIES_ATTRS = {
8
+ max_age: '0',
9
+ value: '',
10
+ expires: Time.at(0)
11
+ }.freeze
15
12
 
16
- def write(header)
17
- @cookies.select { |key, _value| @send_cookies[key] == true }.each do |name, value|
18
- cookie_value = value.is_a?(Hash) ? value : { value: value }
19
- Rack::Utils.set_cookie_header! header, name, cookie_value
20
- end
13
+ def_delegators :cookies, :[], :each
14
+
15
+ def initialize(rack_cookies)
16
+ @cookies = ActiveSupport::HashWithIndifferentAccess.new(rack_cookies)
17
+ @send_cookies = nil
21
18
  end
22
19
 
23
- def [](name)
24
- @cookies[name.to_s]
20
+ def response_cookies
21
+ return unless @send_cookies
22
+
23
+ send_cookies.each do |name|
24
+ yield name, cookies[name]
25
+ end
25
26
  end
26
27
 
27
28
  def []=(name, value)
28
- @cookies[name.to_s] = value
29
- @send_cookies[name.to_s] = true
29
+ cookies[name] = value
30
+ send_cookies << name
30
31
  end
31
32
 
32
- def each(&block)
33
- @cookies.each(&block)
33
+ # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340
34
+ def delete(name, **opts)
35
+ self.[]=(name, opts.merge(DELETED_COOKIES_ATTRS))
34
36
  end
35
37
 
36
- # rubocop:disable Layout/SpaceBeforeBrackets
37
- def delete(name, **opts)
38
- options = opts.merge(value: 'deleted', expires: Time.at(0))
39
- self.[]=(name, options)
38
+ private
39
+
40
+ attr_reader :cookies
41
+
42
+ def send_cookies
43
+ @send_cookies ||= Set.new
40
44
  end
41
- # rubocop:enable Layout/SpaceBeforeBrackets
42
45
  end
43
46
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class DeclaredParamsHandler
5
+ def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map: nil)
6
+ @include_missing = include_missing
7
+ @evaluate_given = evaluate_given
8
+ @stringify = stringify
9
+ @contract_key_map = contract_key_map
10
+ end
11
+
12
+ def call(passed_params, declared_params, route_params, renamed_params)
13
+ recursive_declared(
14
+ passed_params,
15
+ declared_params:,
16
+ route_params:,
17
+ renamed_params:
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def recursive_declared(passed_params, declared_params:, route_params:, renamed_params:, params_nested_path: [])
24
+ res = if passed_params.is_a?(Array)
25
+ passed_params.map do |passed_param|
26
+ recursive_declared(passed_param, declared_params:, params_nested_path:, renamed_params:, route_params:)
27
+ end
28
+ else
29
+ declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
30
+ end
31
+
32
+ @contract_key_map&.each { |key_map| key_map.write(passed_params, res) }
33
+
34
+ res
35
+ end
36
+
37
+ def declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
38
+ declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
39
+ next if @evaluate_given && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
40
+
41
+ declared_hash_attr(
42
+ passed_params,
43
+ declared_param: declared_param_attr.key,
44
+ params_nested_path:,
45
+ memo:,
46
+ renamed_params:,
47
+ route_params:
48
+ )
49
+ end
50
+ end
51
+
52
+ def declared_hash_attr(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
53
+ return declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:) unless declared_param.is_a?(Hash)
54
+
55
+ declared_param.each_pair do |parent, children|
56
+ declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
57
+ end
58
+ end
59
+
60
+ def declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
61
+ return unless @include_missing || passed_params.key?(parent)
62
+
63
+ memo_key = build_memo_key(params_nested_path, parent, renamed_params)
64
+ passed_children = passed_params[parent] || passed_params.class.new
65
+ nested_path = nested_path_for(params_nested_path, parent)
66
+
67
+ memo[memo_key] = handle_passed_param(nested_path, route_params:, has_passed_children: passed_children.any?) do
68
+ recursive_declared(
69
+ passed_children,
70
+ declared_params: children,
71
+ params_nested_path: nested_path,
72
+ renamed_params:,
73
+ route_params:
74
+ )
75
+ end
76
+ end
77
+
78
+ # The declared param has no children. Find its value or set it to nil.
79
+ def declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
80
+ return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param))
81
+
82
+ memo_key = build_memo_key(params_nested_path, declared_param, renamed_params)
83
+ passed_param = passed_params[declared_param]
84
+
85
+ memo[memo_key] = passed_param || handle_passed_param(nested_path_for(params_nested_path, declared_param), route_params:) do
86
+ passed_param
87
+ end
88
+ end
89
+
90
+ def build_memo_key(params_nested_path, declared_param, renamed_params)
91
+ renamed_param_name = renamed_params[nested_path_for(params_nested_path, declared_param)]
92
+ param = renamed_param_name || declared_param
93
+ @stringify ? param.to_s : param.to_sym
94
+ end
95
+
96
+ def nested_path_for(parent_path, key)
97
+ parent_path + [key.to_s]
98
+ end
99
+
100
+ def handle_passed_param(params_nested_path, route_params:, has_passed_children: false)
101
+ return yield if has_passed_children
102
+
103
+ key = params_nested_path.first
104
+ key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
105
+
106
+ type = route_params.dig(key, :type)
107
+ return yield if type.nil?
108
+
109
+ return {} if type == 'Hash' && route_params.keys.none? { |k| k != key && k.start_with?("#{key}[") }
110
+ return [] if type == 'Array' || (type.start_with?('[') && !type.include?(','))
111
+ return Set.new if type == 'Set' || type.start_with?('#<Set', 'Set')
112
+
113
+ yield
114
+ end
115
+ end
116
+ end
@@ -1,12 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry-types'
4
-
5
3
  module Grape
6
4
  module DryTypes
7
- # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
8
- # a container in this case. Check documentation for more information
9
- # https://dry-rb.org/gems/dry-types/1.2/getting-started/
10
- include Dry.Types()
5
+ # https://dry-rb.org/gems/dry-types/main/getting-started/
6
+ # limit to what Grape is using
7
+ include Dry.Types(:params, :coercible, :strict)
8
+
9
+ class StrictCache < Grape::Util::Cache
10
+ MAPPING = {
11
+ Grape::API::Boolean => DryTypes::Strict::Bool,
12
+ BigDecimal => DryTypes::Strict::Decimal,
13
+ Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
14
+ TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
15
+ FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
16
+ }.freeze
17
+
18
+ def initialize
19
+ super
20
+ @cache = Hash.new do |h, strict_type|
21
+ h[strict_type] = MAPPING.fetch(strict_type) do
22
+ DryTypes.wrapped_dry_types_const_get(DryTypes::Strict, strict_type)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class ParamsCache < Grape::Util::Cache
29
+ MAPPING = {
30
+ Grape::API::Boolean => DryTypes::Params::Bool,
31
+ BigDecimal => DryTypes::Params::Decimal,
32
+ Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
33
+ TrueClass => DryTypes::Params::Bool.constrained(eql: true),
34
+ FalseClass => DryTypes::Params::Bool.constrained(eql: false),
35
+ String => DryTypes::Coercible::String
36
+ }.freeze
37
+
38
+ def initialize
39
+ super
40
+ @cache = Hash.new do |h, params_type|
41
+ h[params_type] = MAPPING.fetch(params_type) do
42
+ DryTypes.wrapped_dry_types_const_get(DryTypes::Params, params_type)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.wrapped_dry_types_const_get(dry_type, type)
49
+ dry_type.const_get(type.name, false)
50
+ rescue NameError
51
+ raise ArgumentError, "type `#{type}` should support coercion via `[]`" unless type.respond_to?(:[])
52
+ end
11
53
  end
12
54
  end
@@ -2,66 +2,16 @@
2
2
 
3
3
  module Grape
4
4
  module DSL
5
- # Blocks can be executed before or after every API call, using `before`, `after`,
6
- # `before_validation` and `after_validation`.
7
- #
8
- # Before and after callbacks execute in the following order:
9
- #
10
- # 1. `before`
11
- # 2. `before_validation`
12
- # 3. _validations_
13
- # 4. `after_validation`
14
- # 5. _the API call_
15
- # 6. `after`
16
- #
17
- # Steps 4, 5 and 6 only happen if validation succeeds.
18
5
  module Callbacks
19
- extend ActiveSupport::Concern
6
+ # before: execute the given block before validation, coercion, or any endpoint
7
+ # before_validation: execute the given block after `before`, but prior to validation or coercion
8
+ # after_validation: execute the given block after validations and coercions, but before any endpoint code
9
+ # after: execute the given block after the endpoint code has run except in unsuccessful
10
+ # finally: execute the given block after the endpoint code even if unsuccessful
20
11
 
21
- include Grape::DSL::Configuration
22
-
23
- module ClassMethods
24
- # Execute the given block before validation, coercion, or any endpoint
25
- # code is executed.
26
- def before(&block)
27
- namespace_stackable(:befores, block)
28
- end
29
-
30
- # Execute the given block after `before`, but prior to validation or
31
- # coercion.
32
- def before_validation(&block)
33
- namespace_stackable(:before_validations, block)
34
- end
35
-
36
- # Execute the given block after validations and coercions, but before
37
- # any endpoint code.
38
- def after_validation(&block)
39
- namespace_stackable(:after_validations, block)
40
- end
41
-
42
- # Execute the given block after the endpoint code has run.
43
- def after(&block)
44
- namespace_stackable(:afters, block)
45
- end
46
-
47
- # Allows you to specify a something that will always be executed after a call
48
- # API call. Unlike the `after` block, this code will run even on
49
- # unsuccesful requests.
50
- # @example
51
- # class ExampleAPI < Grape::API
52
- # before do
53
- # ApiLogger.start
54
- # end
55
- # finally do
56
- # ApiLogger.close
57
- # end
58
- # end
59
- #
60
- # This will make sure that the ApiLogger is opened and closed around every
61
- # request
62
- # @param ensured_block [Proc] The block to be executed after every api_call
63
- def finally(&block)
64
- namespace_stackable(:finallies, block)
12
+ %i[before before_validation after_validation after finally].each do |callback_name|
13
+ define_method callback_name do |&block|
14
+ inheritable_setting.add_callback(callback_name, block)
65
15
  end
66
16
  end
67
17
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Declared
6
+ # Denotes a situation where a DSL method has been invoked in a
7
+ # filter which it should not yet be available in
8
+ class MethodNotYetAvailable < StandardError
9
+ def initialize(msg = '#declared is not available prior to parameter validation')
10
+ super
11
+ end
12
+ end
13
+
14
+ # A filtering method that will return a hash
15
+ # consisting only of keys that have been declared by a
16
+ # `params` statement against the current/target endpoint or parent
17
+ # namespaces.
18
+ # @param params [Hash] The initial hash to filter. Usually this will just be `params`
19
+ # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
20
+ # options. `:include_parent_namespaces` defaults to true, hence must be set to false if
21
+ # you want only to return params declared against the current/target endpoint.
22
+ def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
23
+ raise MethodNotYetAvailable unless before_filter_passed
24
+
25
+ contract_key_map = inheritable_setting.contract_key_maps
26
+ handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:)
27
+ declared_params = include_parent_namespaces ? inheritable_setting.route_declared_params : (inheritable_setting.declared_params.last || [])
28
+ renamed_params = inheritable_setting.route_renamed_params
29
+ route_params = config.params
30
+
31
+ handler.call(passed_params, declared_params, route_params, renamed_params)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -3,13 +3,14 @@
3
3
  module Grape
4
4
  module DSL
5
5
  module Desc
6
- include Grape::DSL::Settings
6
+ extend Grape::DSL::Settings
7
7
 
8
8
  # Add a description to the next namespace or function.
9
9
  # @param description [String] descriptive string for this endpoint
10
10
  # or namespace
11
11
  # @param options [Hash] other properties you can set to describe the
12
- # endpoint or namespace. Optional.
12
+ # endpoint or namespace. Optional. Pass these as keyword arguments;
13
+ # passing a positional options Hash is deprecated.
13
14
  # @option options :detail [String] additional detail about this endpoint
14
15
  # @option options :summary [String] summary for this endpoint
15
16
  # @option options :params [Hash] param types and info. normally, you set
@@ -18,6 +19,9 @@ module Grape
18
19
  # successful call to this action
19
20
  # @option options :http_codes [Array[Array]] possible HTTP codes this
20
21
  # endpoint may return, with their meanings, in a 2d array
22
+ # @option options :default_response [Hash] the definition and entity used
23
+ # to present this endpoint's default response. Was `:default`, which is
24
+ # deprecated.
21
25
  # @option options :named [String] a specific name to help find this route
22
26
  # @option options :body_name [String] override the autogenerated body name param
23
27
  # @option options :headers [Hash] HTTP headers this method can accept
@@ -49,70 +53,28 @@ module Grape
49
53
  # # ...
50
54
  # end
51
55
  #
52
- def desc(description, options = {}, &config_block)
53
- if config_block
54
- endpoint_configuration = if defined?(configuration)
55
- # When the instance is mounted - the configuration is executed on mount time
56
- if configuration.respond_to?(:evaluate)
57
- configuration.evaluate
58
- # Within `given` or `mounted blocks` the configuration is already evaluated
59
- elsif configuration.is_a?(Hash)
60
- configuration
61
- end
62
- end
63
- endpoint_configuration ||= {}
64
- config_class = desc_container(endpoint_configuration)
65
-
66
- config_class.configure do
67
- description description
68
- end
69
-
70
- config_class.configure(&config_block)
71
- ActiveSupport::Deprecation.warn('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') if options.any?
72
- options = config_class.settings
73
- else
74
- options = options.merge(description: description)
75
- end
76
-
77
- namespace_setting :description, options
78
- route_setting :description, options
79
- end
80
-
81
- # Returns an object which configures itself via an instance-context DSL.
82
- def desc_container(endpoint_configuration)
83
- Module.new do
84
- include Grape::Util::StrictHashConfiguration.module(
85
- :summary,
86
- :description,
87
- :detail,
88
- :params,
89
- :entity,
90
- :http_codes,
91
- :named,
92
- :body_name,
93
- :headers,
94
- :hidden,
95
- :deprecated,
96
- :is_array,
97
- :nickname,
98
- :produces,
99
- :consumes,
100
- :security,
101
- :tags,
102
- :default
103
- )
104
- config_context.define_singleton_method(:configuration) do
105
- endpoint_configuration
106
- end
107
-
108
- def config_context.success(*args)
109
- entity(*args)
56
+ def desc(description, **options, &config_block)
57
+ resolved_options =
58
+ if options.key?(:default)
59
+ Grape.deprecator.warn('The `default` option of `desc` is deprecated. Use `default_response` instead.')
60
+ # Rebuilt rather than mutated in place: an explicit +default_response+
61
+ # still wins, since the merged Hash is the one that keeps its key.
62
+ { default_response: options[:default] }.merge(options.except(:default))
63
+ else
64
+ options
110
65
  end
111
66
 
112
- def config_context.failure(*args)
113
- http_codes(*args)
67
+ settings =
68
+ if config_block
69
+ endpoint_config = defined?(configuration) ? configuration : nil
70
+ Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings
71
+ else
72
+ resolved_options.merge(description:)
114
73
  end
115
- end
74
+ # Only the route scope is consumed downstream (by +route+ and the
75
+ # route's readers, e.g. +http_codes+); the namespace scope was
76
+ # write-only, so it is no longer populated.
77
+ inheritable_setting.route_description = settings
116
78
  end
117
79
  end
118
80
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Entity
6
+ # Allows you to make use of Grape Entities by setting
7
+ # the response body to the serializable hash of the
8
+ # entity provided in the `:with` option. This has the
9
+ # added benefit of automatically passing along environment
10
+ # and version information to the serialization, making it
11
+ # very easy to do conditional exposures. See Entity docs
12
+ # for more info.
13
+ #
14
+ # @param args [Array] either `(object)` or `(key, object)` where key is a Symbol
15
+ # used to nest the representation under that key in the response body.
16
+ # @param root [Symbol, String, nil] wraps the representation under this root key.
17
+ # @param with [Class, nil] the entity class to use for representation.
18
+ # If omitted, the entity class is inferred from the object via {#entity_class_for_obj}.
19
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
20
+ #
21
+ # @example
22
+ #
23
+ # get '/users/:id' do
24
+ # present User.find(params[:id]),
25
+ # with: API::Entities::User,
26
+ # admin: current_user.admin?
27
+ # end
28
+ def present(*args, root: nil, with: nil, **options)
29
+ key, object = args.count == 2 && args.first.is_a?(Symbol) ? args : [nil, args.first]
30
+ entity_class = with || entity_class_for_obj(object)
31
+ representation = entity_class ? entity_representation_for(entity_class, object, options) : object
32
+ representation = { root => representation } if root
33
+
34
+ if key
35
+ representation = body&.merge(key => representation) || { key => representation }
36
+ elsif entity_class.present? && body
37
+ raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
38
+
39
+ representation = body.merge(representation)
40
+ end
41
+
42
+ body representation
43
+ end
44
+
45
+ # Attempt to locate the Entity class for a given object, if not given
46
+ # explicitly. This is done by looking for the presence of Klass::Entity,
47
+ # where Klass is the class of the `object` parameter, or one of its
48
+ # ancestors. Object is excluded from the search: top-level constants
49
+ # live on it, so a global ::Entity class is not a representer.
50
+ # @param object [Object] the object to locate the Entity class for
51
+ # @return [Class] the located Entity class, or nil if none is found
52
+ def entity_class_for_obj(object)
53
+ entity_for_class(object.class) || entity_for_class(element_class(object))
54
+ end
55
+
56
+ private
57
+
58
+ # The class standing in for a collection or wrapper: ActiveRecord::Relation
59
+ # and the like expose #klass, anything else falls back to the class of its
60
+ # first element.
61
+ #
62
+ # Consulted only once the object's own class has come up empty, because
63
+ # both tests are duck-typed and plenty of single objects answer them —
64
+ # a Struct is Enumerable, so it responds to #first, and so does any model
65
+ # that includes Enumerable. Asking this first meant `represent Model,
66
+ # with: Entity` was silently ignored for those, the entity for the
67
+ # *element* type being looked up instead. Deferring it also keeps #first
68
+ # from being called at all when the object resolves on its own class.
69
+ #
70
+ # @param object [Object] the object to represent.
71
+ # @return [Class, nil]
72
+ def element_class(object)
73
+ return object.klass if object.respond_to?(:klass)
74
+
75
+ object.first.class if object.respond_to?(:first)
76
+ end
77
+
78
+ # @param klass [Class, nil] the class to look an entity up for.
79
+ # @return [Class, nil] the registered or conventionally named entity.
80
+ def entity_for_class(klass)
81
+ return if klass.nil?
82
+
83
+ representations = inheritable_setting.representations
84
+ if representations
85
+ potential = klass.ancestors.detect { |ancestor| representations.key?(ancestor) }
86
+ return representations[potential] if potential && representations[potential]
87
+ end
88
+
89
+ owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
90
+ return unless owner
91
+
92
+ entity = owner.const_get(:Entity, false)
93
+ entity if entity.respond_to?(:represent)
94
+ end
95
+
96
+ # @param entity_class [Class] the entity class to use for representation.
97
+ # @param object [Object] the object to represent.
98
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
99
+ # @return the representation of the given object as done through the given entity_class.
100
+ def entity_representation_for(entity_class, object, options)
101
+ embeds = env.key?(Grape::Env::API_VERSION) ? { env:, version: env[Grape::Env::API_VERSION] } : { env: }
102
+ entity_class.represent(object, **embeds, **options)
103
+ end
104
+ end
105
+ end
106
+ end