grape 2.4.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  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 +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -3,18 +3,24 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class ParamsScope
6
- attr_accessor :element, :parent, :index
7
- attr_reader :type, :params_meeting_dependency
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
8
11
 
9
12
  include Grape::DSL::Parameters
13
+ include Grape::Validations::ParamsDocumentation
10
14
 
11
15
  # There are a number of documentation options on entities that don't have
12
16
  # corresponding validators. Since there is nowhere that enumerates them all,
13
17
  # we maintain a list of them here and skip looking up validators for them.
14
18
  RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
15
19
 
20
+ SPECIAL_JSON = [JSON, Array[JSON]].freeze
21
+
16
22
  class Attr
17
- attr_accessor :key, :scope
23
+ attr_reader :key, :scope
18
24
 
19
25
  # Open up a new ParamsScope::Attr
20
26
  # @param key [Hash, Symbol] key of attr
@@ -32,9 +38,10 @@ module Grape
32
38
  end
33
39
 
34
40
  def self.attr_key(declared_param_attr)
35
- return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
36
-
37
- 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
38
45
  declared_param_attr.transform_values { |value| attrs_keys(value) }
39
46
  else
40
47
  declared_param_attr
@@ -44,40 +51,47 @@ module Grape
44
51
 
45
52
  # Open up a new ParamsScope, allowing parameter definitions per
46
53
  # Grape::DSL::Params.
47
- # @param opts [Hash] options for this scope
48
- # @option opts :element [Symbol] the element that contains this scope; for
49
- # this to be relevant, @parent must be set
50
- # @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
51
58
  # be renamed and to what, given +nil+ no renaming is done
52
- # @option opts :parent [ParamsScope] the scope containing this scope
53
- # @option opts :api [API] the API endpoint to modify
54
- # @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
55
61
  # any parameters set or not
56
- # @option opts :type [Class] a type meant to govern this scope (deprecated)
57
- # @option opts :type [Hash] group options for this scope
58
- # @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
59
65
  # validate if this param is present in the parent scope
60
66
  # @yield the instance context, open for parameter definitions
61
- def initialize(opts, &block)
62
- @element = opts[:element]
63
- @element_renamed = opts[:element_renamed]
64
- @parent = opts[:parent]
65
- @api = opts[:api]
66
- @optional = opts[:optional] || false
67
- @type = opts[:type]
68
- @group = opts[:group]
69
- @dependent_on = opts[:dependent_on]
70
- @params_meeting_dependency = []
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.
71
79
  @declared_params = []
72
- @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
73
84
 
74
85
  instance_eval(&block) if block
75
86
 
76
87
  configure_declared_params
88
+ @nearest_array_ancestor = find_nearest_array_ancestor
89
+ freeze
77
90
  end
78
91
 
79
92
  def configuration
80
- @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
81
95
  end
82
96
 
83
97
  # @return [Boolean] whether or not this entire scope needs to be
@@ -87,9 +101,9 @@ module Grape
87
101
 
88
102
  return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
89
103
  return false unless meets_dependency?(scoped_params, parameters)
90
- return true if parent.nil?
104
+ return true if @parent.nil?
91
105
 
92
- parent.should_validate?(parameters)
106
+ @parent.should_validate?(parameters)
93
107
  end
94
108
 
95
109
  def meets_dependency?(params, request_params)
@@ -97,8 +111,9 @@ module Grape
97
111
  return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
98
112
 
99
113
  if params.is_a?(Array)
100
- @params_meeting_dependency = params.flatten.filter { |param| meets_dependency?(param, request_params) }
101
- return @params_meeting_dependency.present?
114
+ filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
115
+ ParamScopeTracker.current&.store_qualifying_params(self, filtered)
116
+ return filtered.present?
102
117
  end
103
118
 
104
119
  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
@@ -171,37 +196,31 @@ module Grape
171
196
  !@optional
172
197
  end
173
198
 
174
- def reset_index
175
- @index = nil
176
- end
177
-
178
199
  protected
179
200
 
180
201
  # Adds a parameter declaration to our list of validations.
181
202
  # @param attrs [Array] (see Grape::DSL::Parameters#requires)
182
- def push_declared_params(attrs, opts = {})
183
- opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope)
184
- return @parent.push_declared_params(attrs, opts) if lateral?
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
- push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as]
187
- @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
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
- if nested?
195
- (@parent.full_path + [@element])
196
- elsif lateral?
197
- @parent.full_path
198
- else
199
- []
200
- end
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) })
201
213
  end
202
214
 
203
215
  private
204
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
+
205
224
  # Add a new parameter which should be renamed when using the +#declared+
206
225
  # method.
207
226
  #
@@ -210,329 +229,223 @@ module Grape
210
229
  # @param new_name [String, Symbol] the new name of the parameter (the
211
230
  # renamed name, with the +as: ...+ semantic)
212
231
  def push_renamed_param(path, new_name)
213
- base = @api.route_setting(:renamed_params) || {}
214
- base[Array(path).map(&:to_s)] = new_name.to_s
215
- @api.route_setting(:renamed_params, base)
232
+ @api.inheritable_setting.add_route_renamed_param(Array(path).map(&:to_s), new_name.to_s)
216
233
  end
217
234
 
218
- 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
+
219
239
  if context == :all
220
- optional_fields = Array.wrap(opts[:except])
221
- required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) }
240
+ optional_fields = except_fields
241
+ required_fields = using_fields
222
242
  else # context == :none
223
- required_fields = Array.wrap(opts[:except])
224
- optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) }
243
+ required_fields = except_fields
244
+ optional_fields = using_fields
225
245
  end
226
246
  required_fields.each do |field|
227
- field_opts = opts[:using][field]
228
- 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
229
249
 
230
- requires(field, field_opts)
250
+ requires(field, **field_opts)
231
251
  end
232
252
  optional_fields.each do |field|
233
- field_opts = opts[:using][field]
234
- optional(field, field_opts) if field_opts
253
+ field_opts = using[field]
254
+ optional(field, **field_opts) if field_opts
235
255
  end
236
256
  end
237
257
 
238
- def require_optional_fields(context, opts)
239
- optional_fields = opts[:using].keys
258
+ def require_optional_fields(context, using:, except: nil)
259
+ optional_fields = using.keys
240
260
  unless context == :all
241
- except_fields = Array.wrap(opts[:except])
261
+ except_fields = Array.wrap(except)
242
262
  optional_fields.delete_if { |f| except_fields.include?(f) }
243
263
  end
244
264
  optional_fields.each do |field|
245
- field_opts = opts[:using][field]
246
- optional(field, field_opts) if field_opts
265
+ field_opts = using[field]
266
+ optional(field, **field_opts) if field_opts
247
267
  end
248
268
  end
249
269
 
250
- def validate_attributes(attrs, opts, &block)
251
- validations = opts.clone
252
- validations[:type] ||= Array if block
253
- validates(attrs, validations)
254
- end
255
-
256
270
  # Returns a new parameter scope, subordinate to the current one and nested
257
- # under the parameter corresponding to `attrs.first`.
258
- # @param attrs [Array] the attributes passed to the `requires` or
259
- # `optional` invocation that opened this scope.
260
- # @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
261
276
  # is optional or not (and hence, whether this block's params will be).
262
277
  # @yield parameter scope
263
- def new_scope(attrs, optional = false, &block)
264
- # if required params are grouped and no type or unsupported type is provided, raise an error
265
- type = attrs[1] ? attrs[1][:type] : nil
266
- 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
267
284
  raise Grape::Exceptions::MissingGroupType if type.nil?
268
285
  raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
269
286
  end
270
287
 
271
288
  self.class.new(
272
289
  api: @api,
273
- element: attrs.first,
274
- element_renamed: attrs[1][:as],
290
+ element:,
291
+ element_renamed: as,
275
292
  parent: self,
276
- optional: optional,
293
+ optional:,
277
294
  type: type || Array,
278
295
  group: @group,
279
- &block
296
+ &
280
297
  )
281
298
  end
282
299
 
283
300
  # Returns a new parameter scope, not nested under any current-level param
284
301
  # but instead at the same level as the current scope.
285
- # @param options [Hash] options to control how this new scope behaves
286
- # @option options :dependent_on [Symbol] if given, specifies that this
287
- # scope should only validate if this parameter from the above scope is
288
- # 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
289
304
  # @yield parameter scope
290
- def new_lateral_scope(options, &block)
305
+ def new_lateral_scope(dependent_on:, &)
291
306
  self.class.new(
292
307
  api: @api,
293
- element: nil,
294
308
  parent: self,
295
- options: @optional,
309
+ optional: @optional,
296
310
  type: type == Array ? Array : Hash,
297
- dependent_on: options[:dependent_on],
298
- &block
311
+ dependent_on:,
312
+ &
299
313
  )
300
314
  end
301
315
 
302
- # Returns a new parameter scope, subordinate to the current one and nested
303
- # under the parameter corresponding to `attrs.first`.
304
- # @param attrs [Array] the attributes passed to the `requires` or
305
- # `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
306
319
  # @yield parameter scope
307
- def new_group_scope(attrs, &block)
308
- self.class.new(api: @api, parent: self, group: attrs.first, &block)
320
+ def new_group_scope(group, &)
321
+ self.class.new(api: @api, parent: self, group:, &)
309
322
  end
310
323
 
311
- # 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.
312
328
  def configure_declared_params
313
329
  push_renamed_param(full_path, @element_renamed) if @element_renamed
330
+ return @parent.push_declared_params [{ @element => @declared_params }] if nested?
314
331
 
315
- if nested?
316
- @parent.push_declared_params [element => @declared_params]
317
- else
318
- @api.namespace_stackable(:declared_params, @declared_params)
319
- end
320
-
321
- # params were stored in settings, it can be cleaned from the params scope
332
+ @api.inheritable_setting.add_declared_params(@declared_params)
333
+ ensure
322
334
  @declared_params = nil
323
335
  end
324
336
 
325
- def validates(attrs, validations)
326
- doc = AttributesDoc.new @api, self
327
- doc.extract_details validations
328
-
329
- coerce_type = infer_coercion(validations)
330
-
331
- 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
332
342
 
333
- 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
334
350
 
335
- if (values_hash = validations[:values]).is_a? Hash
336
- values = values_hash[:value]
337
- # NB: excepts is deprecated
338
- excepts = values_hash[:except]
339
- else
340
- 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.')
341
365
  end
342
366
 
343
- doc.values = values
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)
344
370
 
345
- except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
371
+ document_params(attrs, spec)
346
372
 
347
- # NB. values and excepts should be nil, Proc, Array, or Range.
348
- # Specifically, values should NOT be a Hash
373
+ # Presence runs first — `required` is forwarded to every subsequent
374
+ # validator (some short-circuit on it).
375
+ validate_presence(spec, attrs)
349
376
 
350
- # use values or excepts to guess coerce type when stated type is Array
351
- coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts)
377
+ # Coerce runs second — later validators see the typed value.
378
+ validate_coerce(spec, attrs)
352
379
 
353
- # default value should be present in values array, if both exist and are not procs
354
- check_incompatible_option_values(default, values, except_values, excepts)
355
-
356
- # type should be compatible with values array, if both exist
357
- validate_value_coercion(coerce_type, values, except_values, excepts)
358
-
359
- doc.document attrs
360
-
361
- opts = derive_validator_options(validations)
362
-
363
- # Validate for presence before any other validators
364
- validates_presence(validations, attrs, doc, opts)
365
-
366
- # Before we run the rest of the validators, let's handle
367
- # whatever coercion so that we are working with correctly
368
- # type casted values
369
- coerce_type validations, attrs, doc, opts
370
-
371
- validations.each do |type, options|
372
- # Don't try to look up validators for documentation params that don't have one.
373
- next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
374
-
375
- validate(type, options, attrs, doc, opts)
376
- end
377
- end
378
-
379
- # Validate and comprehend the +:type+, +:types+, and +:coerce_with+
380
- # options that have been supplied to the parameter declaration.
381
- # The +:type+ and +:types+ options will be removed from the
382
- # validations list, replaced appropriately with +:coerce+ and
383
- # +:coerce_with+ options that will later be passed to
384
- # {Validators::CoerceValidator}. The type that is returned may be
385
- # used for documentation and further validation of parameter
386
- # options.
387
- #
388
- # @param validations [Hash] list of validations supplied to the
389
- # parameter declaration
390
- # @return [class-like] type to which the parameter will be coerced
391
- # @raise [ArgumentError] if the given type options are invalid
392
- def infer_coercion(validations)
393
- raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
394
-
395
- validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
396
- validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
397
- validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
398
- validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
399
-
400
- validations.delete(:types) if validations.key?(:types)
401
-
402
- coerce_type = validations[:coerce]
403
-
404
- # Special case - when the argument is a single type that is a
405
- # variant-type collection.
406
- if Types.multiple?(coerce_type) && validations.key?(:type)
407
- validations[:coerce] = Types::VariantCollectionCoercer.new(
408
- coerce_type,
409
- validations.delete(:coerce_with)
410
- )
380
+ spec.validator_entries.each do |type, options|
381
+ validate(type, options, attrs, spec.required?, spec.shared_opts)
411
382
  end
412
- validations.delete(:type)
413
-
414
- coerce_type
415
383
  end
416
384
 
417
- # Enforce correct usage of :coerce_with parameter.
418
- # We do not allow coercion without a type, nor with
419
- # +JSON+ as a type since this defines its own coercion
420
- # method.
421
- def check_coerce_with(validations)
422
- return unless validations.key?(:coerce_with)
423
- # type must be supplied for coerce_with..
424
- raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
425
-
426
- # but not special JSON types, which
427
- # already imply coercion method
428
- 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)
429
392
 
430
393
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
431
394
  end
432
395
 
433
- # Add type coercion validation to this scope,
434
- # if any has been specified.
435
- # This validation has special handling since it is
436
- # composited from more than one +requires+/+optional+
437
- # parameter, and needs to be run before most other
438
- # validations.
439
- def coerce_type(validations, attrs, doc, opts)
440
- check_coerce_with(validations)
396
+ def validate_presence(spec, attrs)
397
+ return unless spec.required?
441
398
 
442
- return unless validations.key?(:coerce)
443
-
444
- coerce_options = {
445
- type: validations[:coerce],
446
- method: validations[:coerce_with],
447
- message: validations[:coerce_message]
448
- }
449
- validate('coerce', coerce_options, attrs, doc, opts)
450
- validations.delete(:coerce_with)
451
- validations.delete(:coerce)
452
- validations.delete(:coerce_message)
399
+ validate('presence', spec.presence_options, attrs, true, spec.shared_opts)
453
400
  end
454
401
 
455
- def guess_coerce_type(coerce_type, *values_list)
456
- return coerce_type unless coerce_type == Array
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
457
411
 
458
- values_list.each do |values|
459
- next if !values || values.is_a?(Proc)
460
- return values.first.class if values.is_a?(Range) || !values.empty?
461
- end
462
- coerce_type
412
+ validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
463
413
  end
464
414
 
465
- def check_incompatible_option_values(default, values, except_values, excepts)
466
- return unless default && !default.is_a?(Proc)
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
467
426
 
468
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
469
-
470
- if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
471
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
472
- end
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)
473
430
 
474
- return unless excepts && !excepts.is_a?(Proc)
475
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
476
- unless Array(default).none? { |def_val| excepts.include?(def_val) }
431
+ variants.map { |block| OneofCollector.collect(block) }
477
432
  end
478
433
 
479
- def validate(type, options, attrs, doc, opts)
480
- validator_options = {
481
- attributes: attrs,
482
- options: options,
483
- required: doc.required,
484
- params_scope: self,
485
- opts: opts,
486
- validator_class: Validations.require_validator(type)
487
- }
488
- @api.namespace_stackable(:validations, validator_options)
489
- end
490
-
491
- def validate_value_coercion(coerce_type, *values_list)
492
- return unless coerce_type
493
-
494
- coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
495
- values_list.each do |values|
496
- next if !values || values.is_a?(Proc)
497
-
498
- value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
499
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
500
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
501
- end
502
- end
503
-
504
- def extract_message_option(attrs)
505
- return nil unless attrs.is_a?(Array)
506
-
507
- opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
508
- opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
509
- end
510
-
511
- def options_key?(type, key, validations)
512
- 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)
513
444
  end
514
445
 
515
446
  def all_element_blank?(scoped_params)
516
447
  scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
517
448
  end
518
-
519
- # Validators don't have access to each other and they don't need, however,
520
- # some validators might influence others, so their options should be shared
521
- def derive_validator_options(validations)
522
- allow_blank = validations[:allow_blank]
523
-
524
- {
525
- allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
526
- fail_fast: validations.delete(:fail_fast) || false
527
- }
528
- end
529
-
530
- def validates_presence(validations, attrs, doc, opts)
531
- return unless validations.key?(:presence) && validations[:presence]
532
-
533
- validate('presence', validations.delete(:presence), attrs, doc, opts)
534
- validations.delete(:message) if validations.key?(:message)
535
- end
536
449
  end
537
450
  end
538
451
  end