grape 1.8.0 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -1
  3. data/CONTRIBUTING.md +3 -11
  4. data/README.md +648 -552
  5. data/UPGRADING.md +803 -7
  6. data/grape.gemspec +11 -14
  7. data/lib/grape/api/instance.rb +68 -158
  8. data/lib/grape/api.rb +71 -107
  9. data/lib/grape/content_types.rb +23 -11
  10. data/lib/grape/cookies.rb +28 -25
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -6
  13. data/lib/grape/dsl/callbacks.rb +14 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +15 -63
  16. data/lib/grape/dsl/entity.rb +88 -0
  17. data/lib/grape/dsl/headers.rb +2 -2
  18. data/lib/grape/dsl/helpers.rb +83 -64
  19. data/lib/grape/dsl/inside_route.rb +56 -295
  20. data/lib/grape/dsl/logger.rb +4 -9
  21. data/lib/grape/dsl/middleware.rb +22 -40
  22. data/lib/grape/dsl/parameters.rb +50 -82
  23. data/lib/grape/dsl/request_response.rb +144 -146
  24. data/lib/grape/dsl/rescue_options.rb +24 -0
  25. data/lib/grape/dsl/routing.rb +261 -186
  26. data/lib/grape/dsl/settings.rb +29 -135
  27. data/lib/grape/dsl/validations.rb +39 -32
  28. data/lib/grape/dsl/version_options.rb +23 -0
  29. data/lib/grape/endpoint/options.rb +19 -0
  30. data/lib/grape/endpoint.rb +277 -250
  31. data/lib/grape/{util/env.rb → env.rb} +1 -8
  32. data/lib/grape/error_formatter/base.rb +56 -21
  33. data/lib/grape/error_formatter/json.rb +13 -15
  34. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  35. data/lib/grape/error_formatter/txt.rb +12 -18
  36. data/lib/grape/error_formatter/xml.rb +3 -13
  37. data/lib/grape/error_formatter.rb +5 -25
  38. data/lib/grape/exceptions/base.rb +22 -58
  39. data/lib/grape/exceptions/error_response.rb +45 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  41. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  42. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  44. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  45. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  46. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  47. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  48. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  49. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  50. data/lib/grape/exceptions/request_error.rb +11 -0
  51. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  52. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  53. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  54. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  55. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  56. data/lib/grape/exceptions/validation.rb +25 -9
  57. data/lib/grape/exceptions/validation_array_errors.rb +5 -0
  58. data/lib/grape/exceptions/validation_errors.rb +22 -26
  59. data/lib/grape/formatter/base.rb +16 -0
  60. data/lib/grape/formatter/json.rb +4 -6
  61. data/lib/grape/formatter/serializable_hash.rb +6 -10
  62. data/lib/grape/formatter/txt.rb +3 -5
  63. data/lib/grape/formatter/xml.rb +4 -6
  64. data/lib/grape/formatter.rb +7 -25
  65. data/lib/grape/json.rb +46 -0
  66. data/lib/grape/locale/en.yml +43 -42
  67. data/lib/grape/middleware/auth/base.rb +11 -33
  68. data/lib/grape/middleware/auth/dsl.rb +37 -31
  69. data/lib/grape/middleware/auth/strategies.rb +1 -2
  70. data/lib/grape/middleware/base.rb +56 -33
  71. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  72. data/lib/grape/middleware/error.rb +183 -91
  73. data/lib/grape/middleware/formatter.rb +94 -105
  74. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  75. data/lib/grape/middleware/stack.rb +27 -38
  76. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  77. data/lib/grape/middleware/versioner/base.rb +72 -0
  78. data/lib/grape/middleware/versioner/header.rb +44 -129
  79. data/lib/grape/middleware/versioner/param.rb +4 -25
  80. data/lib/grape/middleware/versioner/path.rb +13 -32
  81. data/lib/grape/middleware/versioner.rb +7 -14
  82. data/lib/grape/mountable.rb +22 -0
  83. data/lib/grape/namespace.rb +18 -12
  84. data/lib/grape/params_builder/base.rb +20 -0
  85. data/lib/grape/params_builder/hash.rb +11 -0
  86. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  87. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  88. data/lib/grape/params_builder.rb +15 -0
  89. data/lib/grape/parser/base.rb +16 -0
  90. data/lib/grape/parser/json.rb +6 -8
  91. data/lib/grape/parser/xml.rb +6 -8
  92. data/lib/grape/parser.rb +5 -23
  93. data/lib/grape/path.rb +38 -63
  94. data/lib/grape/railtie.rb +9 -0
  95. data/lib/grape/request.rb +168 -26
  96. data/lib/grape/router/base_route.rb +48 -0
  97. data/lib/grape/router/greedy_route.rb +26 -0
  98. data/lib/grape/router/mustermann_pattern.rb +44 -0
  99. data/lib/grape/router/pattern.rb +30 -34
  100. data/lib/grape/router/route.rb +28 -61
  101. data/lib/grape/router.rb +71 -86
  102. data/lib/grape/serve_stream/file_body.rb +1 -0
  103. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  104. data/lib/grape/serve_stream/stream_response.rb +1 -0
  105. data/lib/grape/testing.rb +33 -0
  106. data/lib/grape/util/api_description.rb +58 -0
  107. data/lib/grape/util/base_inheritable.rb +14 -14
  108. data/lib/grape/util/cache.rb +1 -3
  109. data/lib/grape/util/deep_freeze.rb +35 -0
  110. data/lib/grape/util/endpoint_configuration.rb +1 -1
  111. data/lib/grape/util/header.rb +13 -0
  112. data/lib/grape/util/inheritable_setting.rb +51 -27
  113. data/lib/grape/util/inheritable_values.rb +7 -5
  114. data/lib/grape/util/lazy/base.rb +16 -0
  115. data/lib/grape/util/lazy/block.rb +22 -0
  116. data/lib/grape/util/lazy/value.rb +31 -0
  117. data/lib/grape/util/lazy/value_array.rb +21 -0
  118. data/lib/grape/util/lazy/value_enumerable.rb +31 -0
  119. data/lib/grape/util/lazy/value_hash.rb +21 -0
  120. data/lib/grape/util/media_type.rb +67 -0
  121. data/lib/grape/util/path_normalizer.rb +34 -0
  122. data/lib/grape/util/registry.rb +27 -0
  123. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  124. data/lib/grape/util/stackable_values.rb +12 -14
  125. data/lib/grape/util/translation.rb +42 -0
  126. data/lib/grape/validations/attributes_iterator.rb +43 -27
  127. data/lib/grape/validations/coerce_options.rb +21 -0
  128. data/lib/grape/validations/contract_scope.rb +28 -0
  129. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  130. data/lib/grape/validations/oneof_collector.rb +39 -0
  131. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  132. data/lib/grape/validations/params_documentation.rb +52 -0
  133. data/lib/grape/validations/params_scope.rb +180 -303
  134. data/lib/grape/validations/shared_options.rb +19 -0
  135. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  136. data/lib/grape/validations/types/array_coercer.rb +4 -7
  137. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  138. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  139. data/lib/grape/validations/types/dry_type_coercer.rb +15 -28
  140. data/lib/grape/validations/types/json.rb +0 -2
  141. data/lib/grape/validations/types/primitive_coercer.rb +11 -35
  142. data/lib/grape/validations/types/set_coercer.rb +1 -4
  143. data/lib/grape/validations/types/variant_collection_coercer.rb +11 -0
  144. data/lib/grape/validations/types.rb +29 -54
  145. data/lib/grape/validations/validations_spec.rb +164 -0
  146. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  147. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  148. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  149. data/lib/grape/validations/validators/base.rb +120 -36
  150. data/lib/grape/validations/validators/coerce_validator.rb +18 -36
  151. data/lib/grape/validations/validators/contract_scope_validator.rb +45 -0
  152. data/lib/grape/validations/validators/default_validator.rb +13 -16
  153. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  154. data/lib/grape/validations/validators/except_values_validator.rb +15 -5
  155. data/lib/grape/validations/validators/length_validator.rb +48 -0
  156. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  157. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  158. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  159. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  160. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  161. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  162. data/lib/grape/validations/validators/values_validator.rb +36 -65
  163. data/lib/grape/validations.rb +8 -21
  164. data/lib/grape/version.rb +1 -2
  165. data/lib/grape/xml.rb +17 -0
  166. data/lib/grape.rb +75 -288
  167. metadata +79 -291
  168. data/lib/grape/api/helpers.rb +0 -9
  169. data/lib/grape/dsl/api.rb +0 -19
  170. data/lib/grape/dsl/configuration.rb +0 -15
  171. data/lib/grape/eager_load.rb +0 -20
  172. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  173. data/lib/grape/exceptions/missing_option.rb +0 -11
  174. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  175. data/lib/grape/exceptions/unknown_options.rb +0 -11
  176. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
  177. data/lib/grape/extensions/hash.rb +0 -22
  178. data/lib/grape/extensions/hashie/mash.rb +0 -26
  179. data/lib/grape/http/headers.rb +0 -61
  180. data/lib/grape/middleware/globals.rb +0 -16
  181. data/lib/grape/middleware/helpers.rb +0 -12
  182. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  183. data/lib/grape/router/attribute_translator.rb +0 -63
  184. data/lib/grape/types/invalid_value.rb +0 -8
  185. data/lib/grape/util/json.rb +0 -12
  186. data/lib/grape/util/lazy_block.rb +0 -27
  187. data/lib/grape/util/lazy_object.rb +0 -43
  188. data/lib/grape/util/lazy_value.rb +0 -91
  189. data/lib/grape/util/registrable.rb +0 -15
  190. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  191. data/lib/grape/util/xml.rb +0 -10
  192. data/lib/grape/validations/attributes_doc.rb +0 -58
  193. data/lib/grape/validations/types/build_coercer.rb +0 -94
  194. data/lib/grape/validations/validator_factory.rb +0 -15
  195. data/spec/grape/api/custom_validations_spec.rb +0 -213
  196. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  197. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  198. data/spec/grape/api/documentation_spec.rb +0 -59
  199. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  200. data/spec/grape/api/instance_spec.rb +0 -103
  201. data/spec/grape/api/invalid_format_spec.rb +0 -45
  202. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  203. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  204. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  205. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  206. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  207. data/spec/grape/api/recognize_path_spec.rb +0 -21
  208. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  209. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  210. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  211. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  212. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  213. data/spec/grape/api_remount_spec.rb +0 -509
  214. data/spec/grape/api_spec.rb +0 -4356
  215. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  216. data/spec/grape/dsl/desc_spec.rb +0 -98
  217. data/spec/grape/dsl/headers_spec.rb +0 -62
  218. data/spec/grape/dsl/helpers_spec.rb +0 -100
  219. data/spec/grape/dsl/inside_route_spec.rb +0 -531
  220. data/spec/grape/dsl/logger_spec.rb +0 -24
  221. data/spec/grape/dsl/middleware_spec.rb +0 -60
  222. data/spec/grape/dsl/parameters_spec.rb +0 -180
  223. data/spec/grape/dsl/request_response_spec.rb +0 -225
  224. data/spec/grape/dsl/routing_spec.rb +0 -275
  225. data/spec/grape/dsl/settings_spec.rb +0 -261
  226. data/spec/grape/dsl/validations_spec.rb +0 -55
  227. data/spec/grape/endpoint/declared_spec.rb +0 -846
  228. data/spec/grape/endpoint_spec.rb +0 -1085
  229. data/spec/grape/entity_spec.rb +0 -336
  230. data/spec/grape/exceptions/base_spec.rb +0 -81
  231. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
  232. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  233. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  234. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  235. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  236. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
  237. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  238. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  239. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  240. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  241. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
  242. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  243. data/spec/grape/exceptions/validation_spec.rb +0 -19
  244. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  245. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  246. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  247. data/spec/grape/grape_spec.rb +0 -9
  248. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  249. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  250. data/spec/grape/integration/rack_spec.rb +0 -51
  251. data/spec/grape/loading_spec.rb +0 -44
  252. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  253. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  254. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  255. data/spec/grape/middleware/base_spec.rb +0 -221
  256. data/spec/grape/middleware/error_spec.rb +0 -85
  257. data/spec/grape/middleware/exception_spec.rb +0 -294
  258. data/spec/grape/middleware/formatter_spec.rb +0 -461
  259. data/spec/grape/middleware/globals_spec.rb +0 -30
  260. data/spec/grape/middleware/stack_spec.rb +0 -155
  261. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  262. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  263. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  264. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  265. data/spec/grape/middleware/versioner_spec.rb +0 -21
  266. data/spec/grape/named_api_spec.rb +0 -19
  267. data/spec/grape/parser_spec.rb +0 -86
  268. data/spec/grape/path_spec.rb +0 -252
  269. data/spec/grape/presenters/presenter_spec.rb +0 -71
  270. data/spec/grape/request_spec.rb +0 -126
  271. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  272. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  273. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  274. data/spec/grape/util/stackable_values_spec.rb +0 -128
  275. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  276. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  277. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  278. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
  279. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  280. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
  281. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  282. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  283. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  284. data/spec/grape/validations/types_spec.rb +0 -111
  285. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  286. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  287. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  288. data/spec/grape/validations/validators/base_spec.rb +0 -38
  289. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  290. data/spec/grape/validations/validators/default_spec.rb +0 -463
  291. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  292. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  293. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  294. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  295. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  296. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  297. data/spec/grape/validations/validators/values_spec.rb +0 -733
  298. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  299. data/spec/grape/validations_spec.rb +0 -2030
  300. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  301. data/spec/integration/multi_json/json_spec.rb +0 -7
  302. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  303. data/spec/shared/deprecated_class_examples.rb +0 -16
  304. data/spec/shared/versioning_examples.rb +0 -215
  305. data/spec/spec_helper.rb +0 -52
  306. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  307. data/spec/support/chunks.rb +0 -14
  308. data/spec/support/content_type_helpers.rb +0 -15
  309. data/spec/support/endpoint_faker.rb +0 -25
  310. data/spec/support/file_streamer.rb +0 -13
  311. data/spec/support/integer_helpers.rb +0 -13
  312. data/spec/support/versioned_helpers.rb +0 -55
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Frozen value object holding everything {ParamsScope#validates} needs to
6
+ # know about a single +requires+/+optional+ declaration. Built once from
7
+ # the raw validations hash supplied by the DSL; the raw hash is never
8
+ # mutated.
9
+ #
10
+ # Splits the raw entries into three logical buckets:
11
+ #
12
+ # * Spec-consumed keys (type/types/coerce*, presence/message,
13
+ # default/fail_fast, doc keys) — exposed via named accessors and never
14
+ # handed to validator dispatch.
15
+ # * Shared opts (allow_blank, fail_fast) — read by every validator at
16
+ # construction time via {#shared_opts}.
17
+ # * Validator entries (everything else, e.g. +regexp+, +length+, +values+,
18
+ # +allow_blank+, custom validators) — exposed via {#validator_entries}
19
+ # for the dispatch loop.
20
+ #
21
+ # Same key can land in more than one bucket (e.g. +allow_blank+ is both a
22
+ # shared opt and a validator entry; +length+ is both a doc source and a
23
+ # validator entry).
24
+ class ValidationsSpec
25
+ # Keys consumed by the spec itself; must NOT be dispatched as validators
26
+ # by the caller. Documentation-only keys are filtered through a separate
27
+ # set so that dual-purpose keys (length, default, values, except_values)
28
+ # aren't accidentally swallowed.
29
+ SPEC_CONSUMED_KEYS = %i[
30
+ type types coerce coerce_with coerce_message
31
+ presence message
32
+ fail_fast
33
+ desc description documentation
34
+ ].freeze
35
+
36
+ attr_reader :raw, :coerce_type, :coerce_method, :coerce_message, :presence_options, :values, :except_values, :default, :allow_blank, :fail_fast, :shared_opts, :validator_entries
37
+
38
+ def self.from(validations)
39
+ new(validations)
40
+ end
41
+
42
+ def initialize(raw)
43
+ raise ArgumentError, ':type may not be supplied with :types' if raw.key?(:type) && raw.key?(:types)
44
+
45
+ @raw = raw
46
+ @coerce_type, @coerce_message, @coerce_method = parse_coerce(raw)
47
+ @values = resolve_value(raw[:values])
48
+ @except_values = resolve_value(raw[:except_values])
49
+ @default = raw[:default]
50
+ @presence_options = raw[:presence]
51
+ @allow_blank = resolve_value(raw[:allow_blank])
52
+ @fail_fast = raw[:fail_fast] || false
53
+
54
+ @guessed_coerce_type = guess_coerce_type(declared_coerce_type, @values, @except_values)
55
+ @shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
56
+ @validator_entries = build_validator_entries(raw)
57
+
58
+ validate!
59
+
60
+ freeze
61
+ end
62
+
63
+ def required?
64
+ !@presence_options.nil? && @presence_options != false
65
+ end
66
+
67
+ def coerce_options
68
+ CoerceOptions.new(type: @coerce_type, coerce_method: @coerce_method, message: @coerce_message)
69
+ end
70
+
71
+ private
72
+
73
+ # Cross-field consistency checks on the parsed declaration. Run at
74
+ # construction so an incoherent spec (e.g. a +default+ outside +values+,
75
+ # or +values+ whose elements don't match +type+) can never exist —
76
+ # callers no longer have to remember to invoke these separately.
77
+ #
78
+ # NB. +@guessed_coerce_type+ is used only for this check, never for
79
+ # +coerce_options+ — a bare +type: Array+ combined with scalar
80
+ # +values:+ must still coerce to +Array+ at runtime (only the elements
81
+ # are scalar), so the real +@coerce_type+ is never overwritten by it.
82
+ def validate!
83
+ check_incompatible_option_values(@default, @values, @except_values)
84
+ validate_value_coercion(@guessed_coerce_type, @values, @except_values)
85
+ end
86
+
87
+ def check_incompatible_option_values(default, values, except_values)
88
+ return if default.nil? || default.is_a?(Proc)
89
+
90
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && Array(default).any? { |def_val| !values.include?(def_val) }
91
+
92
+ return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
93
+
94
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
95
+ end
96
+
97
+ def validate_value_coercion(coerce_type, *values_list)
98
+ return unless coerce_type
99
+
100
+ coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
101
+ values_list.each do |values|
102
+ next if !values || values.is_a?(Proc)
103
+
104
+ value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
105
+ value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
106
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
107
+ end
108
+ end
109
+
110
+ def build_validator_entries(raw)
111
+ raw.reject do |k, _|
112
+ SPEC_CONSUMED_KEYS.include?(k) || ParamsScope::RESERVED_DOCUMENTATION_KEYWORDS.include?(k)
113
+ end.freeze
114
+ end
115
+
116
+ def parse_coerce(raw)
117
+ if raw.key?(:type)
118
+ coerce, coerce_message = extract_value_and_message(raw[:type])
119
+ coerce_with = raw[:coerce_with]
120
+ return [Types::VariantCollectionCoercer.new(coerce, coerce_with), coerce_message, nil] if Types.multiple?(coerce)
121
+ elsif raw.key?(:types)
122
+ coerce, coerce_message = extract_value_and_message(raw[:types])
123
+ coerce_with = raw[:coerce_with]
124
+ else
125
+ coerce = raw[:coerce]
126
+ coerce_message = raw[:coerce_message]
127
+ coerce_with = raw[:coerce_with]
128
+ end
129
+
130
+ [coerce, coerce_message, coerce_with]
131
+ end
132
+
133
+ def extract_value_and_message(opt)
134
+ return [opt, nil] unless opt.is_a?(Hash)
135
+
136
+ [opt[:value], opt[:message]]
137
+ end
138
+
139
+ def resolve_value(opt)
140
+ opt.is_a?(Hash) ? opt[:value] : opt
141
+ end
142
+
143
+ # The type as written in the DSL. Identical to +@coerce_type+ except for
144
+ # a multiple-type declaration (+type: [Integer, String]+), where
145
+ # {#parse_coerce} wraps the declared list in a
146
+ # {Types::VariantCollectionCoercer} for runtime coercion — the
147
+ # definition-time coherence checks need the list back, since the wrapper
148
+ # matches nothing under +===+.
149
+ def declared_coerce_type
150
+ @coerce_type.is_a?(Types::VariantCollectionCoercer) ? @coerce_type.types : @coerce_type
151
+ end
152
+
153
+ def guess_coerce_type(coerce_type, *values_list)
154
+ return coerce_type unless coerce_type == Array
155
+
156
+ values_list.each do |values|
157
+ next if !values || values.is_a?(Proc)
158
+ return values.first.class if values.is_a?(Range) || !values.empty?
159
+ end
160
+ coerce_type
161
+ end
162
+ end
163
+ end
164
+ end
@@ -4,11 +4,14 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AllOrNoneOfValidator < MultipleParamsBase
7
+ default_message_key :all_or_none
8
+
7
9
  def validate_params!(params)
8
- keys = keys_in_common(params)
9
- return if keys.empty? || keys.length == all_keys.length
10
+ known_keys = all_keys
11
+ keys = keys_in_common(params, known_keys)
12
+ return if keys.empty? || keys.length == attrs.length
10
13
 
11
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none))
14
+ validation_error!(known_keys)
12
15
  end
13
16
  end
14
17
  end
@@ -4,15 +4,20 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AllowBlankValidator < Base
7
- def validate_param!(attr_name, params)
8
- return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash)
7
+ default_message_key :blank
8
+
9
+ def initialize(attrs, options, required, scope, opts)
10
+ super
11
+ @value = option_value
12
+ end
9
13
 
10
- value = params[attr_name]
11
- value = value.strip if value.respond_to?(:strip)
14
+ def validate_param!(attr_name, params)
15
+ return if @value || !hash_like?(params)
12
16
 
17
+ value = scrub(params[attr_name])
13
18
  return if value == false || value.present?
14
19
 
15
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank))
20
+ validation_error!(attr_name)
16
21
  end
17
22
  end
18
23
  end
@@ -4,10 +4,13 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AtLeastOneOfValidator < MultipleParamsBase
7
+ default_message_key :at_least_one
8
+
7
9
  def validate_params!(params)
8
- return unless keys_in_common(params).empty?
10
+ known_keys = all_keys
11
+ return if hash_like?(params) && known_keys.intersect?(params.keys.map { |attr| scope.full_name(attr) })
9
12
 
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one))
13
+ validation_error!(known_keys)
11
14
  end
12
15
  end
13
16
  end
@@ -3,25 +3,76 @@
3
3
  module Grape
4
4
  module Validations
5
5
  module Validators
6
+ # Base class for all parameter validators.
7
+ #
8
+ # == Freeze contract
9
+ # Validator instances are shared across requests and are frozen after
10
+ # initialization (via +.new+). All inputs (+options+, +opts+, +attrs+)
11
+ # arrive pre-frozen from the DSL boundary, so subclass ivars derived
12
+ # from them are frozen by construction. Lazy ivar assignment
13
+ # (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
6
14
  class Base
15
+ extend Forwardable
16
+ include Grape::Util::Translation
17
+
7
18
  attr_reader :attrs
8
19
 
20
+ # +allow_blank+ / +fail_fast+ are read straight off the internal
21
+ # {Grape::Validations::SharedOptions} value object; no per-validator
22
+ # ivars. The object is built from the +opts+ Hash in #initialize, so
23
+ # the public 5th-argument contract stays a plain Hash.
24
+ def_delegators :@opts, :allow_blank, :fail_fast
25
+
26
+ alias fail_fast? fail_fast
27
+ alias allow_blank? allow_blank
28
+
29
+ class << self
30
+ # Declares the default I18n message key used by +validation_error!+.
31
+ # Subclasses that only need a single fixed error message can declare it
32
+ # at the class level instead of overriding +initialize+:
33
+ #
34
+ # class MyValidator < Grape::Validations::Validators::Base
35
+ # default_message_key :my_error
36
+ # end
37
+ #
38
+ # The key is resolved through +message+, so a per-option +:message+
39
+ # override still takes precedence.
40
+ def default_message_key(key = nil)
41
+ if key
42
+ @default_message_key = key
43
+ else
44
+ @default_message_key || (superclass.respond_to?(:default_message_key) ? superclass.default_message_key : nil)
45
+ end
46
+ end
47
+
48
+ def new(...)
49
+ super.freeze
50
+ end
51
+
52
+ def inherited(klass)
53
+ super
54
+ Validations.register(klass)
55
+ end
56
+ end
57
+
9
58
  # Creates a new Validator from options specified
10
59
  # by a +requires+ or +optional+ directive during
11
60
  # parameter definition.
12
61
  # @param attrs [Array] names of attributes to which the Validator applies
13
- # @param options [Object] implementation-dependent Validator options
62
+ # @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
14
63
  # @param required [Boolean] attribute(s) are required or optional
15
64
  # @param scope [ParamsScope] parent scope for this Validator
16
- # @param opts [Array] additional validation options
17
- def initialize(attrs, options, required, scope, *opts)
18
- @attrs = Array(attrs)
19
- @option = options
65
+ # @param opts [Hash] shared validator options; only +:allow_blank+ and
66
+ # +:fail_fast+ are consulted (other keys ignored, as before)
67
+ def initialize(attrs, options, required, scope, opts)
68
+ @attrs = Array(attrs).freeze
69
+ @options = Grape::Util::DeepFreeze.deep_freeze(options)
70
+ @option = @options # TODO: remove in next major release
20
71
  @required = required
21
72
  @scope = scope
22
- opts = opts.any? ? opts.shift : {}
23
- @fail_fast = opts.fetch(:fail_fast, false)
24
- @allow_blank = opts.fetch(:allow_blank, false)
73
+ @opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))
74
+ @exception_message = message(self.class.default_message_key) if self.class.default_message_key
75
+ @iterator = iterator_class.new(@attrs, @scope).freeze
25
76
  end
26
77
 
27
78
  # Validates a given request.
@@ -30,62 +81,95 @@ module Grape
30
81
  # @raise [Grape::Exceptions::Validation] if validation failed
31
82
  # @return [void]
32
83
  def validate(request)
33
- return unless @scope.should_validate?(request.params)
84
+ return unless scope.should_validate?(request.params)
34
85
 
35
86
  validate!(request.params)
36
87
  end
37
88
 
38
89
  # Validates a given parameter hash.
39
- # @note Override #validate if you need to access the entire request.
90
+ # @note Override #validate_param! for per-parameter validation,
91
+ # or #validate if you need access to the entire request.
40
92
  # @param params [Hash] parameters to validate
41
93
  # @raise [Grape::Exceptions::Validation] if validation failed
42
94
  # @return [void]
43
95
  def validate!(params)
44
- attributes = SingleAttributeIterator.new(self, @scope, params)
45
96
  # we collect errors inside array because
46
97
  # there may be more than one error per field
47
- array_errors = []
98
+ array_errors = nil
48
99
 
49
- attributes.each do |val, attr_name, empty_val|
50
- next if !@scope.required? && empty_val
51
- next unless @scope.meets_dependency?(val, params)
100
+ @iterator.each(params) do |val, attr_name, empty_val|
101
+ next if !scope.required? && empty_val
102
+ next unless scope.meets_dependency?(val, params)
52
103
 
53
- validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name))
104
+ validate_param!(attr_name, val) if required? || (hash_like?(val) && val.key?(attr_name))
54
105
  rescue Grape::Exceptions::Validation => e
55
- array_errors << e
106
+ (array_errors ||= []) << e
56
107
  end
57
108
 
58
- raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
109
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
110
+ end
111
+
112
+ protected
113
+
114
+ # Validates a single attribute. Override in subclasses.
115
+ # @param attr_name [Symbol, String] the attribute name
116
+ # @param params [Hash] the parameter hash containing the attribute
117
+ # @raise [Grape::Exceptions::Validation] if validation failed
118
+ # @return [void]
119
+ def validate_param!(attr_name, params)
120
+ raise NotImplementedError
121
+ end
122
+
123
+ private
124
+
125
+ attr_reader :options, :scope, :required, :exception_message
126
+
127
+ alias required? required
128
+
129
+ # The AttributesIterator subclass used to walk this validator's
130
+ # attributes. Built once in #initialize and reused across requests.
131
+ def iterator_class
132
+ SingleAttributeIterator
133
+ end
134
+
135
+ def validation_error!(attr_name_or_params, message = exception_message)
136
+ params = attr_name_or_params.is_a?(Array) ? attr_name_or_params : scope.full_name(attr_name_or_params)
137
+ raise Grape::Exceptions::Validation.new(params:, message:)
59
138
  end
60
139
 
61
- def self.inherited(klass)
62
- return if klass.name.blank?
140
+ def hash_like?(obj)
141
+ obj.respond_to?(:key?)
142
+ end
63
143
 
64
- short_validator_name = klass.name.demodulize.underscore.delete_suffix('_validator')
65
- Validations.register_validator(short_validator_name, klass)
144
+ def options_key?(key, given_options = nil)
145
+ current_options = given_options || options
146
+ hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
66
147
  end
67
148
 
149
+ # Returns the effective message for a validation error.
150
+ # Prefers an explicit +:message+ option, then +default_key+.
151
+ # If both are nil, the block (if given) is called to compute a fallback —
152
+ # useful for validators that build a message Hash for deferred i18n interpolation.
153
+ # @example
154
+ # @exception_message = message(:presence) # symbol key or custom message
155
+ # @exception_message = message { build_hash_message } # computed fallback
68
156
  def message(default_key = nil)
69
- options = instance_variable_get(:@option)
70
- options_key?(:message) ? options[:message] : default_key
157
+ key = options_key?(:message) ? options[:message] : default_key
158
+ return key unless key.nil?
159
+
160
+ yield if block_given?
71
161
  end
72
162
 
73
- def options_key?(key, options = nil)
74
- options = instance_variable_get(:@option) if options.nil?
75
- options.respond_to?(:key?) && options.key?(key) && !options[key].nil?
163
+ def option_value
164
+ options_key?(:value) ? options[:value] : options
76
165
  end
77
166
 
78
- def fail_fast?
79
- @fail_fast
167
+ def scrub(value)
168
+ return value if !value.respond_to?(:valid_encoding?) || value.valid_encoding?
169
+
170
+ value.scrub
80
171
  end
81
172
  end
82
173
  end
83
174
  end
84
175
  end
85
-
86
- Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do
87
- def self.inherited(*)
88
- ActiveSupport::Deprecation.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.'
89
- super
90
- end
91
- end
@@ -4,22 +4,29 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class CoerceValidator < Base
7
- def initialize(attrs, options, required, scope, **opts)
7
+ default_message_key :coerce
8
+
9
+ def initialize(attrs, options, required, scope, opts)
8
10
  super
9
11
 
10
- @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
11
- type
12
- else
13
- Types.build_coercer(type, method: @option[:method])
14
- end
12
+ @exception_message = options.message if options.message
13
+
14
+ raw_type = options.type
15
+ type = hash_like?(raw_type) ? raw_type[:value] : raw_type
16
+ @converter =
17
+ if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
18
+ type
19
+ else
20
+ Types.build_coercer(type, method: options.coerce_method)
21
+ end
15
22
  end
16
23
 
17
24
  def validate_param!(attr_name, params)
18
- raise validation_exception(attr_name) unless params.is_a? Hash
25
+ validation_error!(attr_name) unless hash_like?(params)
19
26
 
20
27
  new_value = coerce_value(params[attr_name])
21
28
 
22
- raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value)
29
+ validation_error!(attr_name, new_value.message || exception_message) if new_value.is_a?(Types::InvalidValue)
23
30
 
24
31
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
25
32
  # which looses wrappers for hashes and arrays after reassigning values
@@ -37,38 +44,13 @@ module Grape
37
44
 
38
45
  private
39
46
 
40
- # @!attribute [r] converter
41
- # Object that will be used for parameter coercion and type checking.
42
- #
43
- # See {Types.build_coercer}
44
- #
45
- # @return [Object]
46
- attr_reader :converter
47
-
48
- def valid_type?(val)
49
- !val.is_a?(Types::InvalidValue)
50
- end
51
-
47
+ # Calls the converter built at definition time.
48
+ # Custom coercers may raise; any StandardError is treated as an invalid value.
52
49
  def coerce_value(val)
53
- converter.call(val)
54
- # Some custom types might fail, so it should be treated as an invalid value
50
+ @converter.call(val)
55
51
  rescue StandardError
56
52
  Types::InvalidValue.new
57
53
  end
58
-
59
- # Type to which the parameter will be coerced.
60
- #
61
- # @return [Class]
62
- def type
63
- @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type]
64
- end
65
-
66
- def validation_exception(attr_name, custom_msg = nil)
67
- Grape::Exceptions::Validation.new(
68
- params: [@scope.full_name(attr_name)],
69
- message: custom_msg || message(:coerce)
70
- )
71
- end
72
54
  end
73
55
  end
74
56
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Validators
6
+ class ContractScopeValidator
7
+ attr_reader :schema
8
+
9
+ def initialize(schema:)
10
+ @schema = schema
11
+ freeze
12
+ end
13
+
14
+ # Validates a given request.
15
+ # @param request [Grape::Request] the request currently being handled
16
+ # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed
17
+ # @return [void]
18
+ def validate(request)
19
+ res = schema.call(request.params)
20
+
21
+ if res.success?
22
+ request.params.deep_merge!(res.to_h)
23
+ return
24
+ end
25
+
26
+ raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages))
27
+ end
28
+
29
+ def fail_fast?
30
+ false
31
+ end
32
+
33
+ private
34
+
35
+ def build_errors_from_messages(messages)
36
+ messages.map do |message|
37
+ full_name = message.path.first.to_s
38
+ full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1
39
+ Grape::Exceptions::Validation.new(params: full_name, message: message.text)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,27 +4,24 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class DefaultValidator < Base
7
- def initialize(attrs, options, required, scope, **opts)
8
- @default = options
7
+ def initialize(attrs, options, required, scope, opts)
9
8
  super
10
- end
11
-
12
- def validate_param!(attr_name, params)
13
- params[attr_name] = if @default.is_a? Proc
14
- @default.call
15
- elsif @default.frozen? || !@default.duplicable?
16
- @default
17
- else
18
- @default.dup
19
- end
9
+ # !important, lazy call at runtime
10
+ @default_call =
11
+ if options.is_a?(Proc)
12
+ options.arity.zero? ? proc { options.call } : options
13
+ elsif options.duplicable?
14
+ proc { options.dup }
15
+ else
16
+ proc { options }
17
+ end
20
18
  end
21
19
 
22
20
  def validate!(params)
23
- attrs = SingleAttributeIterator.new(self, @scope, params)
24
- attrs.each do |resource_params, attr_name|
25
- next unless @scope.meets_dependency?(resource_params, params)
21
+ @iterator.each(params) do |resource_params, attr_name|
22
+ next unless scope.meets_dependency?(resource_params, params)
26
23
 
27
- validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
24
+ resource_params[attr_name] = @default_call.call(resource_params) if hash_like?(resource_params) && resource_params[attr_name].nil?
28
25
  end
29
26
  end
30
27
  end
@@ -4,12 +4,19 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class ExactlyOneOfValidator < MultipleParamsBase
7
+ def initialize(attrs, options, required, scope, opts)
8
+ super
9
+ @exactly_one_exception_message = message(:exactly_one)
10
+ @mutual_exclusion_exception_message = message(:mutual_exclusion)
11
+ end
12
+
7
13
  def validate_params!(params)
8
- keys = keys_in_common(params)
14
+ known_keys = all_keys
15
+ keys = keys_in_common(params, known_keys)
9
16
  return if keys.length == 1
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero?
11
17
 
12
- raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
18
+ validation_error!(known_keys, @exactly_one_exception_message) if keys.empty?
19
+ validation_error!(keys, @mutual_exclusion_exception_message)
13
20
  end
14
21
  end
15
22
  end
@@ -4,19 +4,29 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class ExceptValuesValidator < Base
7
- def initialize(attrs, options, required, scope, **opts)
8
- @except = options.is_a?(Hash) ? options[:value] : options
7
+ default_message_key :except_values
8
+
9
+ def initialize(attrs, options, required, scope, opts)
9
10
  super
11
+ except = option_value
12
+ except_proc = except.is_a?(Proc)
13
+ raise ArgumentError, 'except_values Proc must have arity of zero (use values: with a one-arity predicate for per-element checks)' if except_proc && !except.arity.zero?
14
+
15
+ # Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
16
+ # not at definition time, so they are wrapped in a lambda to defer execution.
17
+ @excepts_call = except_proc ? except : -> { except }
10
18
  end
11
19
 
12
20
  def validate_param!(attr_name, params)
13
- return unless params.respond_to?(:key?) && params.key?(attr_name)
21
+ return unless hash_like?(params) && params.key?(attr_name)
14
22
 
15
- excepts = @except.is_a?(Proc) ? @except.call : @except
23
+ excepts = @excepts_call.call
16
24
  return if excepts.nil?
17
25
 
18
26
  param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
19
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) }
27
+ return if param_array.none? { |param| excepts.include?(param) }
28
+
29
+ validation_error!(attr_name)
20
30
  end
21
31
  end
22
32
  end