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,22 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'attributes_doc'
4
-
5
3
  module Grape
6
4
  module Validations
7
5
  class ParamsScope
8
- attr_accessor :element, :parent, :index
9
- attr_reader :type
6
+ attr_reader :parent, :type, :nearest_array_ancestor, :array_depth, :full_path
7
+
8
+ def qualifying_params
9
+ ParamScopeTracker.current&.qualifying_params(self)
10
+ end
10
11
 
11
12
  include Grape::DSL::Parameters
13
+ include Grape::Validations::ParamsDocumentation
12
14
 
13
15
  # There are a number of documentation options on entities that don't have
14
16
  # corresponding validators. Since there is nowhere that enumerates them all,
15
17
  # we maintain a list of them here and skip looking up validators for them.
16
18
  RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
17
19
 
20
+ SPECIAL_JSON = [JSON, Array[JSON]].freeze
21
+
18
22
  class Attr
19
- attr_accessor :key, :scope
23
+ attr_reader :key, :scope
20
24
 
21
25
  # Open up a new ParamsScope::Attr
22
26
  # @param key [Hash, Symbol] key of attr
@@ -34,9 +38,10 @@ module Grape
34
38
  end
35
39
 
36
40
  def self.attr_key(declared_param_attr)
37
- return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
38
-
39
- if declared_param_attr.is_a?(Hash)
41
+ case declared_param_attr
42
+ when self
43
+ attr_key(declared_param_attr.key)
44
+ when Hash
40
45
  declared_param_attr.transform_values { |value| attrs_keys(value) }
41
46
  else
42
47
  declared_param_attr
@@ -46,39 +51,47 @@ module Grape
46
51
 
47
52
  # Open up a new ParamsScope, allowing parameter definitions per
48
53
  # Grape::DSL::Params.
49
- # @param opts [Hash] options for this scope
50
- # @option opts :element [Symbol] the element that contains this scope; for
51
- # this to be relevant, @parent must be set
52
- # @option opts :element_renamed [Symbol, nil] whenever this scope should
54
+ # @param api [API] the API endpoint to modify
55
+ # @param element [Symbol] the element that contains this scope; for
56
+ # this to be relevant, parent must be set
57
+ # @param element_renamed [Symbol, nil] whenever this scope should
53
58
  # be renamed and to what, given +nil+ no renaming is done
54
- # @option opts :parent [ParamsScope] the scope containing this scope
55
- # @option opts :api [API] the API endpoint to modify
56
- # @option opts :optional [Boolean] whether or not this scope needs to have
59
+ # @param parent [ParamsScope] the scope containing this scope
60
+ # @param optional [Boolean] whether or not this scope needs to have
57
61
  # any parameters set or not
58
- # @option opts :type [Class] a type meant to govern this scope (deprecated)
59
- # @option opts :type [Hash] group options for this scope
60
- # @option opts :dependent_on [Symbol] if present, this scope should only
62
+ # @param type [Class] a type meant to govern this scope (deprecated)
63
+ # @param type [Hash] group options for this scope
64
+ # @param dependent_on [Symbol] if present, this scope should only
61
65
  # validate if this param is present in the parent scope
62
66
  # @yield the instance context, open for parameter definitions
63
- def initialize(opts, &block)
64
- @element = opts[:element]
65
- @element_renamed = opts[:element_renamed]
66
- @parent = opts[:parent]
67
- @api = opts[:api]
68
- @optional = opts[:optional] || false
69
- @type = opts[:type]
70
- @group = opts[:group]
71
- @dependent_on = opts[:dependent_on]
67
+ def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block)
68
+ @element = element
69
+ @element_renamed = element_renamed
70
+ @parent = parent
71
+ @api = api
72
+ @optional = optional
73
+ @type = type
74
+ @group = group
75
+ @dependent_on = dependent_on
76
+ # Must be an ivar: push_declared_params is dispatched on self during
77
+ # instance_eval, so local variables from initialize are unreachable.
78
+ # configure_declared_params consumes it and clears @declared_params to nil.
72
79
  @declared_params = []
73
- @index = nil
80
+ @full_path = build_full_path
81
+ # Read by the validators instantiated from the block below, so it has to
82
+ # be settled before the instance_eval.
83
+ @array_depth = find_array_depth
74
84
 
75
85
  instance_eval(&block) if block
76
86
 
77
87
  configure_declared_params
88
+ @nearest_array_ancestor = find_nearest_array_ancestor
89
+ freeze
78
90
  end
79
91
 
80
92
  def configuration
81
- @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
93
+ config = @api.configuration
94
+ config.is_a?(Grape::Util::Lazy::Base) ? config.evaluate : config
82
95
  end
83
96
 
84
97
  # @return [Boolean] whether or not this entire scope needs to be
@@ -88,24 +101,26 @@ module Grape
88
101
 
89
102
  return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
90
103
  return false unless meets_dependency?(scoped_params, parameters)
91
- return true if parent.nil?
104
+ return true if @parent.nil?
92
105
 
93
- parent.should_validate?(parameters)
106
+ @parent.should_validate?(parameters)
94
107
  end
95
108
 
96
109
  def meets_dependency?(params, request_params)
97
110
  return true unless @dependent_on
98
-
99
111
  return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
100
112
 
101
- return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
113
+ if params.is_a?(Array)
114
+ filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
115
+ ParamScopeTracker.current&.store_qualifying_params(self, filtered)
116
+ return filtered.present?
117
+ end
102
118
 
103
119
  meets_hash_dependency?(params)
104
120
  end
105
121
 
106
122
  def attr_meets_dependency?(params)
107
123
  return true unless @dependent_on
108
-
109
124
  return false if @parent.present? && !@parent.attr_meets_dependency?(params)
110
125
 
111
126
  meets_hash_dependency?(params)
@@ -115,28 +130,27 @@ module Grape
115
130
  # params might be anything what looks like a hash, so it must implement a `key?` method
116
131
  return false unless params.respond_to?(:key?)
117
132
 
118
- @dependent_on.each do |dependency|
133
+ @dependent_on.all? do |dependency|
119
134
  if dependency.is_a?(Hash)
120
- dependency_key = dependency.keys[0]
121
- proc = dependency.values[0]
122
- return false unless proc.call(params.try(:[], dependency_key))
123
- elsif params.respond_to?(:key?) && params.try(:[], dependency).blank?
124
- return false
135
+ key, callable = dependency.first
136
+ callable.call(params[key])
137
+ else
138
+ params[dependency].present?
125
139
  end
126
140
  end
127
-
128
- true
129
141
  end
130
142
 
131
143
  # @return [String] the proper attribute name, with nesting considered.
132
144
  def full_name(name, index: nil)
145
+ tracker = ParamScopeTracker.current
133
146
  if nested?
134
147
  # Find our containing element's name, and append ours.
135
- "#{@parent.full_name(@element)}#{brackets(@index || index)}#{brackets(name)}"
148
+ resolved_index = index || tracker&.index_for(self)
149
+ "#{@parent.full_name(@element)}#{brackets(resolved_index)}#{brackets(name)}"
136
150
  elsif lateral?
137
151
  # Find the name of the element as if it was at the same nesting level
138
152
  # as our parent. We need to forward our index upward to achieve this.
139
- @parent.full_name(name, index: @index)
153
+ @parent.full_name(name, index: tracker&.index_for(self))
140
154
  else
141
155
  # We must be the root scope, so no prefix needed.
142
156
  name.to_s
@@ -158,6 +172,17 @@ module Grape
158
172
  @parent && @element
159
173
  end
160
174
 
175
+ # Whether this scope's params resolve to one entry per element, which is
176
+ # what makes both an element index and a nesting level meaningful.
177
+ #
178
+ # +type: Array[JSON]+ counts as much as +type: Array+ does. It is easy to
179
+ # miss because it evaluates to the Array *instance* +[JSON]+ rather than
180
+ # the Array class, so an +== Array+ test quietly excluded it.
181
+ # @return [Boolean]
182
+ def iterates_elements?
183
+ @type == Array || @type == SPECIAL_JSON.last
184
+ end
185
+
161
186
  # A lateral scope is subordinate to its parent, but its keys are at the
162
187
  # same level as its parent and thus is not contained within an element.
163
188
  # @return [Boolean] whether or not this scope is lateral
@@ -175,27 +200,27 @@ module Grape
175
200
 
176
201
  # Adds a parameter declaration to our list of validations.
177
202
  # @param attrs [Array] (see Grape::DSL::Parameters#requires)
178
- def push_declared_params(attrs, **opts)
179
- opts = opts.merge(declared_params_scope: self) unless opts.key?(:declared_params_scope)
180
- if lateral?
181
- @parent.push_declared_params(attrs, **opts)
182
- else
183
- push_renamed_param(full_path + [attrs.first], opts[:as]) \
184
- if opts && opts[:as]
203
+ # +declared_params_scope+ is the scope an attribute is recorded against.
204
+ # It defaults to the receiver and is forwarded unchanged when a lateral
205
+ # scope hands the push to its parent, so an attribute declared inside a
206
+ # +given+ block stays attributed to the lateral scope that declared it
207
+ # rather than to the parent that stores it.
208
+ def push_declared_params(attrs, as: nil, declared_params_scope: self)
209
+ return @parent.push_declared_params(attrs, as:, declared_params_scope:) if lateral?
185
210
 
186
- @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
187
- end
188
- end
189
-
190
- # Get the full path of the parameter scope in the hierarchy.
191
- #
192
- # @return [Array<Symbol>] the nesting/path of the current parameter scope
193
- def full_path
194
- nested? ? @parent.full_path + [@element] : []
211
+ push_renamed_param(full_path + [attrs.first], as) if as
212
+ @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, declared_params_scope) })
195
213
  end
196
214
 
197
215
  private
198
216
 
217
+ def build_full_path
218
+ return @parent.full_path + [@element] if nested?
219
+ return @parent.full_path if lateral?
220
+
221
+ []
222
+ end
223
+
199
224
  # Add a new parameter which should be renamed when using the +#declared+
200
225
  # method.
201
226
  #
@@ -204,331 +229,223 @@ module Grape
204
229
  # @param new_name [String, Symbol] the new name of the parameter (the
205
230
  # renamed name, with the +as: ...+ semantic)
206
231
  def push_renamed_param(path, new_name)
207
- base = @api.route_setting(:renamed_params) || {}
208
- base[Array(path).map(&:to_s)] = new_name.to_s
209
- @api.route_setting(:renamed_params, base)
232
+ @api.inheritable_setting.add_route_renamed_param(Array(path).map(&:to_s), new_name.to_s)
210
233
  end
211
234
 
212
- def require_required_and_optional_fields(context, opts)
235
+ def require_required_and_optional_fields(context, using:, except: nil)
236
+ except_fields = Array.wrap(except)
237
+ using_fields = using.keys.delete_if { |f| except_fields.include?(f) }
238
+
213
239
  if context == :all
214
- optional_fields = Array(opts[:except])
215
- required_fields = opts[:using].keys - optional_fields
240
+ optional_fields = except_fields
241
+ required_fields = using_fields
216
242
  else # context == :none
217
- required_fields = Array(opts[:except])
218
- optional_fields = opts[:using].keys - required_fields
243
+ required_fields = except_fields
244
+ optional_fields = using_fields
219
245
  end
220
246
  required_fields.each do |field|
221
- field_opts = opts[:using][field]
222
- raise ArgumentError, "required field not exist: #{field}" unless field_opts
247
+ field_opts = using[field]
248
+ raise ArgumentError, "required field not exist: `#{field}`" unless field_opts
223
249
 
224
- requires(field, field_opts)
250
+ requires(field, **field_opts)
225
251
  end
226
252
  optional_fields.each do |field|
227
- field_opts = opts[:using][field]
228
- optional(field, field_opts) if field_opts
253
+ field_opts = using[field]
254
+ optional(field, **field_opts) if field_opts
229
255
  end
230
256
  end
231
257
 
232
- def require_optional_fields(context, opts)
233
- optional_fields = opts[:using].keys
234
- optional_fields -= Array(opts[:except]) unless context == :all
258
+ def require_optional_fields(context, using:, except: nil)
259
+ optional_fields = using.keys
260
+ unless context == :all
261
+ except_fields = Array.wrap(except)
262
+ optional_fields.delete_if { |f| except_fields.include?(f) }
263
+ end
235
264
  optional_fields.each do |field|
236
- field_opts = opts[:using][field]
237
- optional(field, field_opts) if field_opts
265
+ field_opts = using[field]
266
+ optional(field, **field_opts) if field_opts
238
267
  end
239
268
  end
240
269
 
241
- def validate_attributes(attrs, opts, &block)
242
- validations = opts.clone
243
- validations[:type] ||= Array if block
244
- validates(attrs, validations)
245
- end
246
-
247
270
  # Returns a new parameter scope, subordinate to the current one and nested
248
- # under the parameter corresponding to `attrs.first`.
249
- # @param attrs [Array] the attributes passed to the `requires` or
250
- # `optional` invocation that opened this scope.
251
- # @param optional [Boolean] whether the parameter this are nested under
271
+ # under the given element.
272
+ # @param element [Symbol] the parameter name under which this scope is nested
273
+ # @param type [Class] the type governing this scope
274
+ # @param as [Symbol, nil] optional renamed name for the element
275
+ # @param optional [Boolean] whether the parameter this scope is nested under
252
276
  # is optional or not (and hence, whether this block's params will be).
253
277
  # @yield parameter scope
254
- def new_scope(attrs, optional = false, &block)
255
- # if required params are grouped and no type or unsupported type is provided, raise an error
256
- type = attrs[1] ? attrs[1][:type] : nil
257
- if attrs.first && !optional
278
+ def new_scope(element, type:, as:, optional: false, &)
279
+ # A group needs a type: it says whether the nested params sit under one
280
+ # object or repeat in a list, and without it the `type || Array` below
281
+ # would quietly pick one. Checked here for `requires` and `optional`
282
+ # alike — `new_scope` is only reached from their block branch.
283
+ if element
258
284
  raise Grape::Exceptions::MissingGroupType if type.nil?
259
285
  raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
260
286
  end
261
287
 
262
288
  self.class.new(
263
289
  api: @api,
264
- element: attrs.first,
265
- element_renamed: attrs[1][:as],
290
+ element:,
291
+ element_renamed: as,
266
292
  parent: self,
267
- optional: optional,
293
+ optional:,
268
294
  type: type || Array,
269
- &block
295
+ group: @group,
296
+ &
270
297
  )
271
298
  end
272
299
 
273
300
  # Returns a new parameter scope, not nested under any current-level param
274
301
  # but instead at the same level as the current scope.
275
- # @param options [Hash] options to control how this new scope behaves
276
- # @option options :dependent_on [Symbol] if given, specifies that this
277
- # scope should only validate if this parameter from the above scope is
278
- # present
302
+ # @param dependent_on [Symbol] if given, specifies that this scope should
303
+ # only validate if this parameter from the above scope is present
279
304
  # @yield parameter scope
280
- def new_lateral_scope(options, &block)
305
+ def new_lateral_scope(dependent_on:, &)
281
306
  self.class.new(
282
307
  api: @api,
283
- element: nil,
284
308
  parent: self,
285
- options: @optional,
309
+ optional: @optional,
286
310
  type: type == Array ? Array : Hash,
287
- dependent_on: options[:dependent_on],
288
- &block
311
+ dependent_on:,
312
+ &
289
313
  )
290
314
  end
291
315
 
292
- # Returns a new parameter scope, subordinate to the current one and nested
293
- # under the parameter corresponding to `attrs.first`.
294
- # @param attrs [Array] the attributes passed to the `requires` or
295
- # `optional` invocation that opened this scope.
316
+ # Returns a new parameter scope, subordinate to the current one, sharing
317
+ # the given group options with all parameters defined within.
318
+ # @param group [Hash] common options to merge into each parameter in the scope
296
319
  # @yield parameter scope
297
- def new_group_scope(attrs, &block)
298
- self.class.new(
299
- api: @api,
300
- parent: self,
301
- group: attrs.first,
302
- &block
303
- )
320
+ def new_group_scope(group, &)
321
+ self.class.new(api: @api, parent: self, group:, &)
304
322
  end
305
323
 
306
- # Pushes declared params to parent or settings
324
+ # Pushes declared params to parent or settings, then clears @declared_params.
325
+ # Clearing here (rather than in initialize) keeps the lifecycle ownership in
326
+ # one place: this method both consumes and invalidates the ivar so that
327
+ # push_declared_params cannot be called on the frozen scope later.
307
328
  def configure_declared_params
308
329
  push_renamed_param(full_path, @element_renamed) if @element_renamed
330
+ return @parent.push_declared_params [{ @element => @declared_params }] if nested?
309
331
 
310
- if nested?
311
- @parent.push_declared_params [element => @declared_params]
312
- else
313
- @api.namespace_stackable(:declared_params, @declared_params)
314
- end
315
-
316
- # params were stored in settings, it can be cleaned from the params scope
332
+ @api.inheritable_setting.add_declared_params(@declared_params)
333
+ ensure
317
334
  @declared_params = nil
318
335
  end
319
336
 
320
- def validates(attrs, validations)
321
- doc = AttributesDoc.new @api, self
322
- doc.extract_details validations
323
-
324
- coerce_type = infer_coercion(validations)
325
-
326
- doc.type = coerce_type
337
+ def find_nearest_array_ancestor
338
+ scope = @parent
339
+ scope = scope.parent while scope && !scope.iterates_elements?
340
+ scope
341
+ end
327
342
 
328
- default = validations[:default]
343
+ # Every element-iterating scope on the chain adds one level of nesting to
344
+ # what {#params} returns, because +map_params+ maps over the array it
345
+ # resolved from the parent. Counting them tells {AttributesIterator} how
346
+ # deep the declaration says the params for this scope may legitimately be.
347
+ def find_array_depth
348
+ (iterates_elements? ? 1 : 0) + (@parent&.array_depth || 0)
349
+ end
329
350
 
330
- if (values_hash = validations[:values]).is_a? Hash
331
- values = values_hash[:value]
332
- # NB: excepts is deprecated
333
- excepts = values_hash[:except]
334
- else
335
- values = validations[:values]
351
+ # +required+ is the DSL's own signal — +requires+ passes true, +optional+
352
+ # false. It used to travel as a +:presence+ key that +requires+ wrote into
353
+ # the caller's option Hash, which is why a user-supplied +presence:+ was
354
+ # silently overwritten there and silently honoured by +optional+, where
355
+ # nothing overwrote it. The key is built here from the flag instead, after
356
+ # the caller has merged in any enclosing +with+ attributes, so a
357
+ # group-level +message:+ reaches the presence validator.
358
+ #
359
+ # A +presence:+ supplied by an API still decides the outcome exactly as it
360
+ # used to — deprecated rather than dropped, so nothing changes under an
361
+ # API that relies on it until the key is ignored outright.
362
+ def validates(attrs, validations, required: false)
363
+ if validations.key?(:presence)
364
+ Grape.deprecator.warn('Passing a `presence` option is deprecated and it will be ignored in a future release. Declare the parameter with `requires` to make it required, `optional` to make it optional.')
336
365
  end
337
366
 
338
- doc.values = values
339
-
340
- except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
341
-
342
- # NB. values and excepts should be nil, Proc, Array, or Range.
343
- # Specifically, values should NOT be a Hash
344
-
345
- # use values or excepts to guess coerce type when stated type is Array
346
- coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts)
367
+ declared = required ? validations.merge(presence: { value: true, message: validations[:message] }) : validations
368
+ declared = declared.merge(oneof: collected_oneof(declared)) if declared.key?(:oneof)
369
+ spec = ValidationsSpec.from(declared)
347
370
 
348
- # default value should be present in values array, if both exist and are not procs
349
- check_incompatible_option_values(default, values, except_values, excepts)
371
+ document_params(attrs, spec)
350
372
 
351
- # type should be compatible with values array, if both exist
352
- validate_value_coercion(coerce_type, values, except_values, excepts)
373
+ # Presence runs first — `required` is forwarded to every subsequent
374
+ # validator (some short-circuit on it).
375
+ validate_presence(spec, attrs)
353
376
 
354
- doc.document attrs
377
+ # Coerce runs second — later validators see the typed value.
378
+ validate_coerce(spec, attrs)
355
379
 
356
- opts = derive_validator_options(validations)
357
-
358
- # Validate for presence before any other validators
359
- validates_presence(validations, attrs, doc, opts)
360
-
361
- # Before we run the rest of the validators, let's handle
362
- # whatever coercion so that we are working with correctly
363
- # type casted values
364
- coerce_type validations, attrs, doc, opts
365
-
366
- validations.each do |type, options|
367
- # Don't try to look up validators for documentation params that don't have one.
368
- next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
369
-
370
- validate(type, options, attrs, doc, opts)
380
+ spec.validator_entries.each do |type, options|
381
+ validate(type, options, attrs, spec.required?, spec.shared_opts)
371
382
  end
372
383
  end
373
384
 
374
- # Validate and comprehend the +:type+, +:types+, and +:coerce_with+
375
- # options that have been supplied to the parameter declaration.
376
- # The +:type+ and +:types+ options will be removed from the
377
- # validations list, replaced appropriately with +:coerce+ and
378
- # +:coerce_with+ options that will later be passed to
379
- # {Validators::CoerceValidator}. The type that is returned may be
380
- # used for documentation and further validation of parameter
381
- # options.
382
- #
383
- # @param validations [Hash] list of validations supplied to the
384
- # parameter declaration
385
- # @return [class-like] type to which the parameter will be coerced
386
- # @raise [ArgumentError] if the given type options are invalid
387
- def infer_coercion(validations)
388
- raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
389
-
390
- validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
391
- validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
392
- validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
393
- validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
394
-
395
- validations.delete(:types) if validations.key?(:types)
396
-
397
- coerce_type = validations[:coerce]
398
-
399
- # Special case - when the argument is a single type that is a
400
- # variant-type collection.
401
- if Types.multiple?(coerce_type) && validations.key?(:type)
402
- validations[:coerce] = Types::VariantCollectionCoercer.new(
403
- coerce_type,
404
- validations.delete(:coerce_with)
405
- )
406
- end
407
- validations.delete(:type)
408
-
409
- coerce_type
410
- end
411
-
412
- # Enforce correct usage of :coerce_with parameter.
413
- # We do not allow coercion without a type, nor with
414
- # +JSON+ as a type since this defines its own coercion
415
- # method.
416
- def check_coerce_with(validations)
417
- return unless validations.key?(:coerce_with)
418
- # type must be supplied for coerce_with..
419
- raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
420
-
421
- # but not special JSON types, which
422
- # already imply coercion method
423
- return if [JSON, Array[JSON]].exclude? validations[:coerce]
385
+ # Enforce correct usage of :coerce_with on a CoerceOptions.
386
+ # We do not allow coercion without a type, nor with +JSON+ as a type
387
+ # since that defines its own coercion method.
388
+ def check_coerce_with(coerce_options)
389
+ return unless coerce_options.coerce_method
390
+ raise ArgumentError, 'must supply type for coerce_with' unless coerce_options.type
391
+ return unless SPECIAL_JSON.include?(coerce_options.type)
424
392
 
425
393
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
426
394
  end
427
395
 
428
- # Add type coercion validation to this scope,
429
- # if any has been specified.
430
- # This validation has special handling since it is
431
- # composited from more than one +requires+/+optional+
432
- # parameter, and needs to be run before most other
433
- # validations.
434
- def coerce_type(validations, attrs, doc, opts)
435
- check_coerce_with(validations)
436
-
437
- return unless validations.key?(:coerce)
438
-
439
- coerce_options = {
440
- type: validations[:coerce],
441
- method: validations[:coerce_with],
442
- message: validations[:coerce_message]
443
- }
444
- validate('coerce', coerce_options, attrs, doc, opts)
445
- validations.delete(:coerce_with)
446
- validations.delete(:coerce)
447
- validations.delete(:coerce_message)
448
- end
449
-
450
- def guess_coerce_type(coerce_type, *values_list)
451
- return coerce_type unless coerce_type == Array
396
+ def validate_presence(spec, attrs)
397
+ return unless spec.required?
452
398
 
453
- values_list.each do |values|
454
- next if !values || values.is_a?(Proc)
455
- return values.first.class if values.is_a?(Range) || !values.empty?
456
- end
457
- coerce_type
399
+ validate('presence', spec.presence_options, attrs, true, spec.shared_opts)
458
400
  end
459
401
 
460
- def check_incompatible_option_values(default, values, except_values, excepts)
461
- return unless default && !default.is_a?(Proc)
462
-
463
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
464
-
465
- if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
466
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \
467
-
468
- end
469
-
470
- return unless excepts && !excepts.is_a?(Proc)
471
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
472
- unless Array(default).none? { |def_val| excepts.include?(def_val) }
473
- end
402
+ def validate_coerce(spec, attrs)
403
+ coerce_options = spec.coerce_options
404
+ check_coerce_with(coerce_options)
405
+ # Falsy check is intentional: when a remountable API is first evaluated
406
+ # on its base instance (no configuration supplied yet),
407
+ # configuration[:some_type] evaluates to nil. Skipping instantiation
408
+ # here is correct — the real mounted instance will replay this step
409
+ # with the actual type value.
410
+ return unless coerce_options.type
474
411
 
475
- def validate(type, options, attrs, doc, opts)
476
- validator_options = {
477
- attributes: attrs,
478
- options: options,
479
- required: doc.required,
480
- params_scope: self,
481
- opts: opts,
482
- validator_class: Validations.require_validator(type)
483
- }
484
- @api.namespace_stackable(:validations, validator_options)
412
+ validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
485
413
  end
486
414
 
487
- def validate_value_coercion(coerce_type, *values_list)
488
- return unless coerce_type
415
+ # Translate a `oneof: [proc, proc, ...]` declaration into a list of
416
+ # captured validator arrays — one array per variant. Each variant's
417
+ # block is evaluated in its own +ParamsScope+ backed by an
418
+ # {OneofCollector} so the full params DSL is available inside variants
419
+ # and the resulting validators are kept out of the real API's
420
+ # registration list.
421
+ # Returns the collected variants rather than writing them back into
422
+ # +validations+, which is the options Hash the +requires+/+optional+ call
423
+ # site built.
424
+ def collected_oneof(validations)
425
+ raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
489
426
 
490
- coerce_type = coerce_type.first if coerce_type.is_a?(Array)
491
- values_list.each do |values|
492
- next if !values || values.is_a?(Proc)
427
+ variants = validations[:oneof]
428
+ raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
429
+ raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
493
430
 
494
- value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
495
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
496
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
497
- end
431
+ variants.map { |block| OneofCollector.collect(block) }
498
432
  end
499
433
 
500
- def extract_message_option(attrs)
501
- return nil unless attrs.is_a?(Array)
502
-
503
- opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
504
- opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
505
- end
506
-
507
- def options_key?(type, key, validations)
508
- validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
434
+ def validate(type, options, attrs, required, opts)
435
+ validator_class = Validations.require_validator(type)
436
+ validator_instance = validator_class.new(
437
+ attrs,
438
+ options,
439
+ required,
440
+ self,
441
+ opts
442
+ )
443
+ @api.inheritable_setting.add_validation(validator_instance)
509
444
  end
510
445
 
511
446
  def all_element_blank?(scoped_params)
512
447
  scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
513
448
  end
514
-
515
- # Validators don't have access to each other and they don't need, however,
516
- # some validators might influence others, so their options should be shared
517
- def derive_validator_options(validations)
518
- allow_blank = validations[:allow_blank]
519
-
520
- {
521
- allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
522
- fail_fast: validations.delete(:fail_fast) || false
523
- }
524
- end
525
-
526
- def validates_presence(validations, attrs, doc, opts)
527
- return unless validations.key?(:presence) && validations[:presence]
528
-
529
- validate(:presence, validations.delete(:presence), attrs, doc, opts)
530
- validations.delete(:message) if validations.key?(:message)
531
- end
532
449
  end
533
450
  end
534
451
  end