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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Immutable value object holding the two options every validator reads at
6
+ # construction time: +allow_blank+ and +fail_fast+. Internal to
7
+ # {Validators::Base}, which builds it from the +opts+ Hash so the public
8
+ # 5th-argument contract stays a plain Hash — not part of any wire contract.
9
+ #
10
+ # Defaults mirror the prior +opts.values_at+ behaviour: +allow_blank+ is
11
+ # +nil+ when the declaration didn't supply it (validators treat nil as
12
+ # "not set"), +fail_fast+ defaults to +false+.
13
+ SharedOptions = Data.define(:allow_blank, :fail_fast) do
14
+ def initialize(allow_blank: nil, fail_fast: false)
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,12 +5,14 @@ module Grape
5
5
  class SingleAttributeIterator < AttributesIterator
6
6
  private
7
7
 
8
- def yield_attributes(val, attrs)
8
+ def yield_attributes(val)
9
9
  return if skip?(val)
10
10
 
11
- attrs.each do |attr_name|
12
- yield val, attr_name, empty?(val)
13
- end
11
+ # The emptiness of the scope's params is the same answer for every
12
+ # attribute in it, and answering it costs a +respond_to?+, so it is
13
+ # asked once for the whole list rather than once per attribute.
14
+ empty_val = empty?(val)
15
+ @attrs.each { |attr_name| yield val, attr_name, empty_val }
14
16
  end
15
17
 
16
18
  # Primitives like Integers and Booleans don't respond to +empty?+.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'dry_type_coercer'
4
-
5
3
  module Grape
6
4
  module Validations
7
5
  module Types
@@ -9,16 +7,17 @@ module Grape
9
7
  # an array of arrays of integers.
10
8
  #
11
9
  # It could've been possible to use an +of+
12
- # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
10
+ # method (https://dry-rb.org/gems/dry-types/main/array-with-member/)
13
11
  # provided by dry-types. Unfortunately, it doesn't work for Grape because of
14
12
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
15
13
  # maintains Virtus behavior in coercing.
16
14
  class ArrayCoercer < DryTypeCoercer
17
- def initialize(type, strict = false)
15
+ def initialize(type, strict: false)
18
16
  super
19
-
20
- @coercer = scope::Array
21
- @subtype = type.first
17
+ @coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
18
+ # Built eagerly: instances are shared across requests (and cached in
19
+ # Types::CoercerCache), so no lazy state may be created at call time.
20
+ @elem_coercer = DryTypeCoercer.coercer_instance_for(type.first, strict:)
22
21
  end
23
22
 
24
23
  def call(_val)
@@ -30,7 +29,7 @@ module Grape
30
29
 
31
30
  protected
32
31
 
33
- attr_reader :subtype
32
+ attr_reader :elem_coercer
34
33
 
35
34
  def coerce_elements(collection)
36
35
  return if collection.nil?
@@ -53,10 +52,6 @@ module Grape
53
52
  def reject?(val)
54
53
  val.nil?
55
54
  end
56
-
57
- def elem_coercer
58
- @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
59
- end
60
55
  end
61
56
  end
62
57
  end
@@ -33,6 +33,12 @@ module Grape
33
33
  # contract as +coerced?+, and must be supplied with a coercion
34
34
  # +method+.
35
35
  class CustomTypeCoercer
36
+ extend Grape::Util::FreezeOnNew
37
+
38
+ TYPE_CHECK_METHODS = %i[coerced? parsed?].freeze
39
+ COLLECTION_TYPES = [Array, Set].freeze
40
+ private_constant :TYPE_CHECK_METHODS, :COLLECTION_TYPES
41
+
36
42
  # A new coercer for the given type specification
37
43
  # and coercion method.
38
44
  #
@@ -41,8 +47,7 @@ module Grape
41
47
  # @param method [#parse,#call]
42
48
  # optional coercion method. See class docs.
43
49
  def initialize(type, method = nil)
44
- coercion_method = infer_coercion_method type, method
45
- @method = enforce_symbolized_keys type, coercion_method
50
+ @method = build_coercion_method(type, method)
46
51
  @type_check = infer_type_check(type)
47
52
  end
48
53
 
@@ -66,97 +71,54 @@ module Grape
66
71
 
67
72
  private
68
73
 
69
- # Determine the coercion method we're expected to use
70
- # based on the parameters given.
71
- #
72
- # @param type see #new
73
- # @param method see #new
74
- # @return [#call] coercion method
74
+ def build_coercion_method(type, method)
75
+ coercion_method = infer_coercion_method(type, method)
76
+ return hash_symbolizer(coercion_method) if type == Hash
77
+ return collection_symbolizer(coercion_method) if COLLECTION_TYPES.include?(type)
78
+
79
+ coercion_method
80
+ end
81
+
75
82
  def infer_coercion_method(type, method)
76
- if method
77
- if method.respond_to? :parse
78
- method.method :parse
79
- else
80
- method
81
- end
82
- else
83
- # Try to use parse() declared on the target type.
84
- # This may raise an exception, but we are out of ideas anyway.
85
- type.method :parse
86
- end
83
+ return type.method(:parse) unless method
84
+ return method unless method.respond_to?(:parse)
85
+
86
+ method.method(:parse)
87
+ end
88
+
89
+ def hash_symbolizer(method)
90
+ ->(val) { method.call(val).deep_symbolize_keys }
91
+ end
92
+
93
+ # +dup+ before +map!+: the collection is whatever the user's coercion
94
+ # method returned — possibly frozen, possibly the input itself — so we
95
+ # must not mutate it in place. +dup+ also preserves the class, keeping a
96
+ # +Set+ a +Set+ (unlike +map+, which would return an +Array+).
97
+ def collection_symbolizer(method)
98
+ ->(val) { method.call(val).dup.map! { |item| symbolize_if_hash(item) } }
99
+ end
100
+
101
+ def symbolize_if_hash(item)
102
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
87
103
  end
88
104
 
89
- # Determine how the type validity of a coerced
90
- # value should be decided.
91
- #
92
- # @param type see #new
93
- # @return [#call] a procedure which accepts a single parameter
94
- # and returns +true+ if the passed object is of the correct type.
95
105
  def infer_type_check(type)
96
- # First check for special class methods
97
- if type.respond_to? :coerced?
98
- type.method :coerced?
99
- elsif type.respond_to? :parsed?
100
- type.method :parsed?
101
- elsif type.respond_to? :call
102
- # Arbitrary proc passed for type validation.
103
- # Note that this will fail unless a method is also
104
- # passed, or if the type also implements a parse() method.
105
- type
106
- elsif type.is_a?(Enumerable)
107
- lambda do |value|
108
- value.is_a?(Enumerable) && value.all? do |val|
109
- recursive_type_check(type.first, val)
110
- end
111
- end
112
- else
113
- # By default, do a simple type check
114
- ->(value) { value.is_a? type }
115
- end
106
+ method_name = TYPE_CHECK_METHODS.detect { |m| type.respond_to?(m) }
107
+ return type.method(method_name) if method_name
108
+ return type if type.respond_to?(:call)
109
+ return enumerable_type_check(type) if type.is_a?(Enumerable)
110
+
111
+ ->(value) { value.is_a? type }
116
112
  end
117
113
 
118
- def recursive_type_check(type, value)
119
- if type.is_a?(Enumerable) && value.is_a?(Enumerable)
120
- value.all? { |val| recursive_type_check(type.first, val) }
121
- else
122
- !type.is_a?(Enumerable) && value.is_a?(type)
123
- end
114
+ def enumerable_type_check(type)
115
+ ->(value) { value.is_a?(Enumerable) && value.all? { |val| recursive_type_check(type.first, val) } }
124
116
  end
125
117
 
126
- # Enforce symbolized keys for complex types
127
- # by wrapping the coercion method such that
128
- # any Hash objects in the immediate heirarchy
129
- # have their keys recursively symbolized.
130
- # This helps common libs such as JSON to work easily.
131
- #
132
- # @param type see #new
133
- # @param method see #infer_coercion_method
134
- # @return [#call] +method+ wrapped in an additional
135
- # key-conversion step, or just returns +method+
136
- # itself if no conversion is deemed to be
137
- # necessary.
138
- def enforce_symbolized_keys(type, method)
139
- # Collections have all values processed individually
140
- if [Array, Set].include?(type)
141
- lambda do |val|
142
- method.call(val).tap do |new_val|
143
- new_val.map do |item|
144
- item.is_a?(Hash) ? item.deep_symbolize_keys : item
145
- end
146
- end
147
- end
148
-
149
- # Hash objects are processed directly
150
- elsif type == Hash
151
- lambda do |val|
152
- method.call(val).deep_symbolize_keys
153
- end
154
-
155
- # Simple types are not processed.
156
- # This includes Array<primitive> types.
157
- else
158
- method
159
- end
118
+ def recursive_type_check(type, value)
119
+ return value.all? { |val| recursive_type_check(type.first, val) } if type.is_a?(Enumerable) && value.is_a?(Enumerable)
120
+
121
+ !type.is_a?(Enumerable) && value.is_a?(type)
160
122
  end
161
123
  end
162
124
  end
@@ -29,7 +29,7 @@ module Grape
29
29
  # @param set [Boolean]
30
30
  # when true, a +Set+ will be returned by {#call} instead
31
31
  # of an +Array+ and duplicate items will be discarded.
32
- def initialize(type, set = false)
32
+ def initialize(type, set: false)
33
33
  super(type)
34
34
  @set = set
35
35
  end
@@ -1,22 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry-types'
4
-
5
- module DryTypes
6
- # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
7
- # a container in this case. Check documentation for more information
8
- # https://dry-rb.org/gems/dry-types/1.2/getting-started/
9
- include Dry.Types()
10
- end
11
-
12
3
  module Grape
13
4
  module Validations
14
5
  module Types
15
6
  # A base class for classes which must identify a coercer to be used.
16
7
  # If the +strict+ argument is true, it won't coerce the given value
17
8
  # but check its type. More information there
18
- # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
9
+ # https://dry-rb.org/gems/dry-types/main/built-in-types/
19
10
  class DryTypeCoercer
11
+ extend Grape::Util::FreezeOnNew
12
+
20
13
  class << self
21
14
  # Returns a collection coercer which corresponds to a given type.
22
15
  # Example:
@@ -24,31 +17,27 @@ module Grape
24
17
  # collection_coercer_for(Array)
25
18
  # #=> Grape::Validations::Types::ArrayCoercer
26
19
  def collection_coercer_for(type)
27
- collection_coercers.fetch(type) do
28
- DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get("#{type.name.camelize}Coercer")
20
+ case type
21
+ when Array
22
+ ArrayCoercer
23
+ when Set
24
+ SetCoercer
25
+ else
26
+ raise ArgumentError, "unknown type: `#{type}`"
29
27
  end
30
28
  end
31
29
 
32
30
  # Returns an instance of a coercer for a given type
33
- def coercer_instance_for(type, strict = false)
34
- return PrimitiveCoercer.new(type, strict) if type.instance_of?(Class)
35
-
36
- # in case of a collection (Array[Integer]) the type is an instance of a collection,
37
- # so we need to figure out the actual type
38
- collection_coercer_for(type.class).new(type, strict)
39
- end
40
-
41
- protected
42
-
43
- def collection_coercers
44
- @collection_coercers ||= {}
31
+ def coercer_instance_for(type, strict: false)
32
+ klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type)
33
+ klass.new(type, strict:)
45
34
  end
46
35
  end
47
36
 
48
- def initialize(type, strict = false)
37
+ def initialize(type, strict: false)
49
38
  @type = type
50
39
  @strict = strict
51
- @scope = strict ? DryTypes::Strict : DryTypes::Params
40
+ @cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
52
41
  end
53
42
 
54
43
  # Coerces the given value to a type which was specified during
@@ -59,13 +48,13 @@ module Grape
59
48
  return if val.nil?
60
49
 
61
50
  @coercer[val]
62
- rescue Dry::Types::CoercionError => _e
51
+ rescue Dry::Types::CoercionError
63
52
  InvalidValue.new
64
53
  end
65
54
 
66
55
  protected
67
56
 
68
- attr_reader :scope, :type, :strict
57
+ attr_reader :type, :strict, :cache_coercer
69
58
  end
70
59
  end
71
60
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Grape
6
4
  module Validations
7
5
  module Types
@@ -19,9 +17,7 @@ module Grape
19
17
  # @return [Hash,Array<Hash>,nil]
20
18
  def parse(input)
21
19
  return input if parsed?(input)
22
-
23
- # Allow nulls and blank strings
24
- return if input.nil? || input.match?(/^\s*$/)
20
+ return if input.blank?
25
21
 
26
22
  JSON.parse(input, symbolize_names: true)
27
23
  end
@@ -13,6 +13,8 @@ module Grape
13
13
  # an allowed type it should be declared last, since it will always
14
14
  # successfully "coerce" the value.
15
15
  class MultipleTypeCoercer
16
+ extend Grape::Util::FreezeOnNew
17
+
16
18
  # Construct a new coercer that will attempt to coerce
17
19
  # values to the given list of types in the given order.
18
20
  #
@@ -28,7 +30,7 @@ module Grape
28
30
  else
29
31
  Types.build_coercer type, strict: !@method.nil?
30
32
  end
31
- end
33
+ end.freeze
32
34
  end
33
35
 
34
36
  # Coerces the given value.
@@ -39,12 +41,12 @@ module Grape
39
41
  # of {InvalidValue} if the value could not be coerced.
40
42
  def call(val)
41
43
  # once the value is coerced by the custom method, its type should be checked
42
- val = @method.call(val) if @method
44
+ candidate = @method ? @method.call(val) : val
43
45
 
44
46
  coerced_val = InvalidValue.new
45
47
 
46
48
  @type_coercers.each do |coercer|
47
- coerced_val = coercer.call(val)
49
+ coerced_val = coercer.call(candidate)
48
50
 
49
51
  return coerced_val unless coerced_val.is_a?(InvalidValue)
50
52
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'dry_type_coercer'
4
-
5
3
  module Grape
6
4
  module Validations
7
5
  module Types
@@ -9,42 +7,23 @@ module Grape
9
7
  # initialization. When +strict+ is true, it doesn't coerce a value but check
10
8
  # that it has the proper type.
11
9
  class PrimitiveCoercer < DryTypeCoercer
12
- MAPPING = {
13
- Grape::API::Boolean => DryTypes::Params::Bool,
14
- BigDecimal => DryTypes::Params::Decimal,
15
- Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
16
- TrueClass => DryTypes::Params::Bool.constrained(eql: true),
17
- FalseClass => DryTypes::Params::Bool.constrained(eql: false),
18
-
19
- # unfortunately, a +Params+ scope doesn't contain String
20
- String => DryTypes::Coercible::String
21
- }.freeze
22
-
23
- STRICT_MAPPING = {
24
- Grape::API::Boolean => DryTypes::Strict::Bool,
25
- BigDecimal => DryTypes::Strict::Decimal,
26
- Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
27
- TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
28
- FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
29
- }.freeze
10
+ # The input classes Virtus refused for a given declared type, keyed by
11
+ # that type. Resolved once in #initialize rather than re-derived from
12
+ # +type+ on every value: the answer only ever depends on the
13
+ # declaration, and every coerced attribute of every request asks.
14
+ REJECTED_INPUTS = { String => [Array, Hash].freeze, Hash => [String].freeze }.freeze
30
15
 
31
- def initialize(type, strict = false)
16
+ def initialize(type, strict: false)
32
17
  super
33
18
 
34
- @type = type
35
-
36
- @coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
37
- scope.const_get(type.name, false)
38
- rescue NameError
39
- raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
40
-
41
- type
42
- end
19
+ @coercer = cache_coercer[type]
20
+ @rejected_inputs = REJECTED_INPUTS[type]
21
+ @treat_empty_as_nil = type != String
43
22
  end
44
23
 
45
24
  def call(val)
46
25
  return InvalidValue.new if reject?(val)
47
- return nil if val.nil? || treat_as_nil?(val)
26
+ return if val.nil? || treat_as_nil?(val)
48
27
 
49
28
  super
50
29
  end
@@ -58,16 +37,16 @@ module Grape
58
37
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
59
38
  # breaking changes.
60
39
  def reject?(val)
61
- (val.is_a?(Array) && type == String) ||
62
- (val.is_a?(String) && type == Hash) ||
63
- (val.is_a?(Hash) && type == String)
40
+ return false unless @rejected_inputs
41
+
42
+ @rejected_inputs.any? { |klass| val.is_a?(klass) }
64
43
  end
65
44
 
66
45
  # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
67
46
  # absence of a value and coerces it into nil. See a discussion there
68
47
  # https://github.com/ruby-grape/grape/pull/2045
69
48
  def treat_as_nil?(val)
70
- val == '' && type != String
49
+ @treat_empty_as_nil && val == ''
71
50
  end
72
51
  end
73
52
  end
@@ -1,15 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
- require_relative 'array_coercer'
5
-
6
3
  module Grape
7
4
  module Validations
8
5
  module Types
9
6
  # Takes the given array and converts it to a set. Every element of the set
10
7
  # is also coerced.
11
8
  class SetCoercer < ArrayCoercer
12
- def initialize(type, strict = false)
9
+ def initialize(type, strict: false)
13
10
  super
14
11
 
15
12
  @coercer = nil
@@ -6,6 +6,11 @@ module Grape
6
6
  # This class wraps {MultipleTypeCoercer}, for use with collections
7
7
  # that allow members of more than one type.
8
8
  class VariantCollectionCoercer
9
+ extend Grape::Util::FreezeOnNew
10
+
11
+ # @return [Array<Class>,Set<Class>] the member types as declared in the DSL
12
+ attr_reader :types
13
+
9
14
  # Construct a new coercer that will attempt to coerce
10
15
  # a list of values such that all members are of one of
11
16
  # the given types. The container may also optionally be
@@ -26,6 +31,14 @@ module Grape
26
31
  @member_coercer = MultipleTypeCoercer.new types, method
27
32
  end
28
33
 
34
+ # Returns the Grape DSL notation for this coercer, e.g. "Array[Integer, String]".
35
+ # Distinct from the plain-array string "[Integer, String]" produced by the
36
+ # +types:+ keyword, which lets documentation tools tell the two apart.
37
+ def to_s
38
+ container = @types.is_a?(Set) ? 'Set' : 'Array'
39
+ "#{container}[#{@types.join(', ')}]"
40
+ end
41
+
29
42
  # Coerce the given value.
30
43
  #
31
44
  # @param value [Array<String>] collection of values to be coerced
@@ -35,15 +48,15 @@ module Grape
35
48
  def call(value)
36
49
  return unless value.is_a? Array
37
50
 
38
- value =
51
+ coerced =
39
52
  if @method
40
53
  @method.call(value)
41
54
  else
42
55
  value.map { |v| @member_coercer.call(v) }
43
56
  end
44
- return Set.new value if @types.is_a? Set
57
+ return Set.new coerced if @types.is_a? Set
45
58
 
46
- value
59
+ coerced
47
60
  end
48
61
  end
49
62
  end