grape 2.4.0 → 3.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +137 -181
  5. data/UPGRADING.md +154 -0
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -104
  8. data/lib/grape/api.rb +29 -42
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dry_types.rb +48 -4
  12. data/lib/grape/dsl/callbacks.rb +8 -58
  13. data/lib/grape/dsl/declared.rb +35 -0
  14. data/lib/grape/dsl/desc.rb +8 -67
  15. data/lib/grape/dsl/helpers.rb +59 -64
  16. data/lib/grape/dsl/inside_route.rb +28 -189
  17. data/lib/grape/dsl/logger.rb +3 -6
  18. data/lib/grape/dsl/middleware.rb +22 -40
  19. data/lib/grape/dsl/parameters.rb +24 -51
  20. data/lib/grape/dsl/request_response.rb +136 -139
  21. data/lib/grape/dsl/routing.rb +240 -200
  22. data/lib/grape/dsl/settings.rb +23 -135
  23. data/lib/grape/dsl/validations.rb +38 -44
  24. data/lib/grape/endpoint.rb +169 -205
  25. data/lib/grape/error_formatter/base.rb +4 -2
  26. data/lib/grape/exceptions/base.rb +19 -45
  27. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  28. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  29. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  30. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  31. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  32. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  33. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  34. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  35. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  36. data/lib/grape/exceptions/request_error.rb +11 -0
  37. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  38. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  39. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  40. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  41. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  42. data/lib/grape/exceptions/validation.rb +7 -4
  43. data/lib/grape/exceptions/validation_errors.rb +13 -7
  44. data/lib/grape/locale/en.yml +0 -5
  45. data/lib/grape/middleware/auth/base.rb +2 -0
  46. data/lib/grape/middleware/auth/dsl.rb +9 -10
  47. data/lib/grape/middleware/base.rb +6 -4
  48. data/lib/grape/middleware/error.rb +4 -14
  49. data/lib/grape/middleware/formatter.rb +11 -7
  50. data/lib/grape/middleware/stack.rb +3 -2
  51. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  52. data/lib/grape/middleware/versioner/base.rb +42 -40
  53. data/lib/grape/middleware/versioner/header.rb +2 -18
  54. data/lib/grape/middleware/versioner/param.rb +2 -2
  55. data/lib/grape/middleware/versioner/path.rb +2 -2
  56. data/lib/grape/namespace.rb +15 -8
  57. data/lib/grape/params_builder/base.rb +2 -0
  58. data/lib/grape/params_builder.rb +2 -19
  59. data/lib/grape/request.rb +2 -10
  60. data/lib/grape/router/base_route.rb +14 -5
  61. data/lib/grape/router/greedy_route.rb +11 -5
  62. data/lib/grape/router/pattern.rb +6 -20
  63. data/lib/grape/router/route.rb +7 -11
  64. data/lib/grape/router.rb +42 -65
  65. data/lib/grape/util/api_description.rb +58 -0
  66. data/lib/grape/util/base_inheritable.rb +5 -2
  67. data/lib/grape/util/cache.rb +1 -0
  68. data/lib/grape/util/deep_freeze.rb +35 -0
  69. data/lib/grape/util/inheritable_setting.rb +8 -1
  70. data/lib/grape/util/media_type.rb +2 -2
  71. data/lib/grape/util/registry.rb +1 -1
  72. data/lib/grape/util/translation.rb +42 -0
  73. data/lib/grape/validations/attributes_iterator.rb +35 -20
  74. data/lib/grape/validations/contract_scope.rb +2 -8
  75. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  76. data/lib/grape/validations/param_scope_tracker.rb +57 -0
  77. data/lib/grape/validations/params_documentation.rb +50 -0
  78. data/lib/grape/validations/params_scope.rb +147 -156
  79. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  80. data/lib/grape/validations/types/array_coercer.rb +2 -3
  81. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  82. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  83. data/lib/grape/validations/types.rb +10 -25
  84. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  85. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  86. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  87. data/lib/grape/validations/validators/base.rb +95 -25
  88. data/lib/grape/validations/validators/coerce_validator.rb +15 -35
  89. data/lib/grape/validations/validators/contract_scope_validator.rb +9 -5
  90. data/lib/grape/validations/validators/default_validator.rb +12 -18
  91. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  92. data/lib/grape/validations/validators/except_values_validator.rb +13 -4
  93. data/lib/grape/validations/validators/length_validator.rb +21 -22
  94. data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
  95. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  96. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  97. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  98. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  99. data/lib/grape/validations/validators/values_validator.rb +29 -21
  100. data/lib/grape/version.rb +1 -1
  101. data/lib/grape.rb +29 -23
  102. metadata +31 -26
  103. data/lib/grape/api/helpers.rb +0 -9
  104. data/lib/grape/dsl/api.rb +0 -17
  105. data/lib/grape/dsl/configuration.rb +0 -15
  106. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  107. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  108. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  109. data/lib/grape/exceptions/missing_option.rb +0 -11
  110. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  111. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  112. data/lib/grape/exceptions/unknown_options.rb +0 -11
  113. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  114. data/lib/grape/extensions/hash.rb +0 -27
  115. data/lib/grape/extensions/hashie/mash.rb +0 -24
  116. data/lib/grape/types/invalid_value.rb +0 -8
  117. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  118. data/lib/grape/validations/attributes_doc.rb +0 -60
  119. 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, :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
@@ -44,40 +50,43 @@ module Grape
44
50
 
45
51
  # Open up a new ParamsScope, allowing parameter definitions per
46
52
  # 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
53
+ # @param api [API] the API endpoint to modify
54
+ # @param element [Symbol] the element that contains this scope; for
55
+ # this to be relevant, parent must be set
56
+ # @param element_renamed [Symbol, nil] whenever this scope should
51
57
  # 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
58
+ # @param parent [ParamsScope] the scope containing this scope
59
+ # @param optional [Boolean] whether or not this scope needs to have
55
60
  # 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
61
+ # @param type [Class] a type meant to govern this scope (deprecated)
62
+ # @param type [Hash] group options for this scope
63
+ # @param dependent_on [Symbol] if present, this scope should only
59
64
  # validate if this param is present in the parent scope
60
65
  # @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 = []
66
+ def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block)
67
+ @element = element
68
+ @element_renamed = element_renamed
69
+ @parent = parent
70
+ @api = api
71
+ @optional = optional
72
+ @type = type
73
+ @group = group
74
+ @dependent_on = dependent_on
75
+ # Must be an ivar: push_declared_params is dispatched on self during
76
+ # instance_eval, so local variables from initialize are unreachable.
77
+ # configure_declared_params consumes it and clears @declared_params to nil.
71
78
  @declared_params = []
72
- @index = nil
79
+ @full_path = build_full_path
73
80
 
74
81
  instance_eval(&block) if block
75
82
 
76
83
  configure_declared_params
84
+ @nearest_array_ancestor = find_nearest_array_ancestor
85
+ freeze
77
86
  end
78
87
 
79
88
  def configuration
80
- @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
89
+ (@api.configuration.respond_to?(:evaluate) && @api.configuration.evaluate) || @api.configuration
81
90
  end
82
91
 
83
92
  # @return [Boolean] whether or not this entire scope needs to be
@@ -87,9 +96,9 @@ module Grape
87
96
 
88
97
  return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
89
98
  return false unless meets_dependency?(scoped_params, parameters)
90
- return true if parent.nil?
99
+ return true if @parent.nil?
91
100
 
92
- parent.should_validate?(parameters)
101
+ @parent.should_validate?(parameters)
93
102
  end
94
103
 
95
104
  def meets_dependency?(params, request_params)
@@ -97,8 +106,9 @@ module Grape
97
106
  return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
98
107
 
99
108
  if params.is_a?(Array)
100
- @params_meeting_dependency = params.flatten.filter { |param| meets_dependency?(param, request_params) }
101
- return @params_meeting_dependency.present?
109
+ filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
110
+ ParamScopeTracker.current&.store_qualifying_params(self, filtered)
111
+ return filtered.present?
102
112
  end
103
113
 
104
114
  meets_hash_dependency?(params)
@@ -115,28 +125,27 @@ module Grape
115
125
  # params might be anything what looks like a hash, so it must implement a `key?` method
116
126
  return false unless params.respond_to?(:key?)
117
127
 
118
- @dependent_on.each do |dependency|
128
+ @dependent_on.all? do |dependency|
119
129
  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
130
+ key, callable = dependency.first
131
+ callable.call(params[key])
132
+ else
133
+ params[dependency].present?
125
134
  end
126
135
  end
127
-
128
- true
129
136
  end
130
137
 
131
138
  # @return [String] the proper attribute name, with nesting considered.
132
139
  def full_name(name, index: nil)
140
+ tracker = ParamScopeTracker.current
133
141
  if nested?
134
142
  # Find our containing element's name, and append ours.
135
- "#{@parent.full_name(@element)}#{brackets(index || @index)}#{brackets(name)}"
143
+ resolved_index = index || tracker&.index_for(self)
144
+ "#{@parent.full_name(@element)}#{brackets(resolved_index)}#{brackets(name)}"
136
145
  elsif lateral?
137
146
  # Find the name of the element as if it was at the same nesting level
138
147
  # as our parent. We need to forward our index upward to achieve this.
139
- @parent.full_name(name, index: @index)
148
+ @parent.full_name(name, index: tracker&.index_for(self))
140
149
  else
141
150
  # We must be the root scope, so no prefix needed.
142
151
  name.to_s
@@ -171,28 +180,23 @@ module Grape
171
180
  !@optional
172
181
  end
173
182
 
174
- def reset_index
175
- @index = nil
176
- end
177
-
178
183
  protected
179
184
 
180
185
  # Adds a parameter declaration to our list of validations.
181
186
  # @param attrs [Array] (see Grape::DSL::Parameters#requires)
182
- def push_declared_params(attrs, opts = {})
187
+ def push_declared_params(attrs, **opts)
183
188
  opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope)
184
- return @parent.push_declared_params(attrs, opts) if lateral?
189
+ return @parent.push_declared_params(attrs, **opts) if lateral?
185
190
 
186
191
  push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as]
187
192
  @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
188
193
  end
189
194
 
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
195
+ private
196
+
197
+ def build_full_path
194
198
  if nested?
195
- (@parent.full_path + [@element])
199
+ @parent.full_path + [@element]
196
200
  elsif lateral?
197
201
  @parent.full_path
198
202
  else
@@ -200,8 +204,6 @@ module Grape
200
204
  end
201
205
  end
202
206
 
203
- private
204
-
205
207
  # Add a new parameter which should be renamed when using the +#declared+
206
208
  # method.
207
209
  #
@@ -210,169 +212,160 @@ module Grape
210
212
  # @param new_name [String, Symbol] the new name of the parameter (the
211
213
  # renamed name, with the +as: ...+ semantic)
212
214
  def push_renamed_param(path, new_name)
213
- base = @api.route_setting(:renamed_params) || {}
215
+ api_route_setting = @api.inheritable_setting.route
216
+ base = api_route_setting[:renamed_params] || {}
214
217
  base[Array(path).map(&:to_s)] = new_name.to_s
215
- @api.route_setting(:renamed_params, base)
218
+ api_route_setting[:renamed_params] = base
216
219
  end
217
220
 
218
- def require_required_and_optional_fields(context, opts)
221
+ def require_required_and_optional_fields(context, using:, except: nil)
222
+ except_fields = Array.wrap(except)
223
+ using_fields = using.keys.delete_if { |f| except_fields.include?(f) }
224
+
219
225
  if context == :all
220
- optional_fields = Array.wrap(opts[:except])
221
- required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) }
226
+ optional_fields = except_fields
227
+ required_fields = using_fields
222
228
  else # context == :none
223
- required_fields = Array.wrap(opts[:except])
224
- optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) }
229
+ required_fields = except_fields
230
+ optional_fields = using_fields
225
231
  end
226
232
  required_fields.each do |field|
227
- field_opts = opts[:using][field]
233
+ field_opts = using[field]
228
234
  raise ArgumentError, "required field not exist: #{field}" unless field_opts
229
235
 
230
- requires(field, field_opts)
236
+ requires(field, **field_opts)
231
237
  end
232
238
  optional_fields.each do |field|
233
- field_opts = opts[:using][field]
234
- optional(field, field_opts) if field_opts
239
+ field_opts = using[field]
240
+ optional(field, **field_opts) if field_opts
235
241
  end
236
242
  end
237
243
 
238
- def require_optional_fields(context, opts)
239
- optional_fields = opts[:using].keys
244
+ def require_optional_fields(context, using:, except: nil)
245
+ optional_fields = using.keys
240
246
  unless context == :all
241
- except_fields = Array.wrap(opts[:except])
247
+ except_fields = Array.wrap(except)
242
248
  optional_fields.delete_if { |f| except_fields.include?(f) }
243
249
  end
244
250
  optional_fields.each do |field|
245
- field_opts = opts[:using][field]
246
- optional(field, field_opts) if field_opts
251
+ field_opts = using[field]
252
+ optional(field, **field_opts) if field_opts
247
253
  end
248
254
  end
249
255
 
250
- def validate_attributes(attrs, opts, &block)
251
- validations = opts.clone
252
- validations[:type] ||= Array if block
253
- validates(attrs, validations)
256
+ def validate_attributes(attrs, **opts, &block)
257
+ opts[:type] ||= Array if block
258
+ validates(attrs, opts)
254
259
  end
255
260
 
256
261
  # 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
262
+ # under the given element.
263
+ # @param element [Symbol] the parameter name under which this scope is nested
264
+ # @param type [Class] the type governing this scope
265
+ # @param as [Symbol, nil] optional renamed name for the element
266
+ # @param optional [Boolean] whether the parameter this scope is nested under
261
267
  # is optional or not (and hence, whether this block's params will be).
262
268
  # @yield parameter scope
263
- def new_scope(attrs, optional = false, &block)
269
+ def new_scope(element, type:, as:, optional: false, &)
264
270
  # 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
271
+ if element && !optional
267
272
  raise Grape::Exceptions::MissingGroupType if type.nil?
268
273
  raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
269
274
  end
270
275
 
271
276
  self.class.new(
272
277
  api: @api,
273
- element: attrs.first,
274
- element_renamed: attrs[1][:as],
278
+ element:,
279
+ element_renamed: as,
275
280
  parent: self,
276
- optional: optional,
281
+ optional:,
277
282
  type: type || Array,
278
283
  group: @group,
279
- &block
284
+ &
280
285
  )
281
286
  end
282
287
 
283
288
  # Returns a new parameter scope, not nested under any current-level param
284
289
  # 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
290
+ # @param dependent_on [Symbol] if given, specifies that this scope should
291
+ # only validate if this parameter from the above scope is present
289
292
  # @yield parameter scope
290
- def new_lateral_scope(options, &block)
293
+ def new_lateral_scope(dependent_on:, &)
291
294
  self.class.new(
292
295
  api: @api,
293
- element: nil,
294
296
  parent: self,
295
- options: @optional,
297
+ optional: @optional,
296
298
  type: type == Array ? Array : Hash,
297
- dependent_on: options[:dependent_on],
298
- &block
299
+ dependent_on:,
300
+ &
299
301
  )
300
302
  end
301
303
 
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.
304
+ # Returns a new parameter scope, subordinate to the current one, sharing
305
+ # the given group options with all parameters defined within.
306
+ # @param group [Hash] common options to merge into each parameter in the scope
306
307
  # @yield parameter scope
307
- def new_group_scope(attrs, &block)
308
- self.class.new(api: @api, parent: self, group: attrs.first, &block)
308
+ def new_group_scope(group, &)
309
+ self.class.new(api: @api, parent: self, group:, &)
309
310
  end
310
311
 
311
- # Pushes declared params to parent or settings
312
+ # Pushes declared params to parent or settings, then clears @declared_params.
313
+ # Clearing here (rather than in initialize) keeps the lifecycle ownership in
314
+ # one place: this method both consumes and invalidates the ivar so that
315
+ # push_declared_params cannot be called on the frozen scope later.
312
316
  def configure_declared_params
313
317
  push_renamed_param(full_path, @element_renamed) if @element_renamed
314
318
 
315
319
  if nested?
316
- @parent.push_declared_params [element => @declared_params]
320
+ @parent.push_declared_params [@element => @declared_params]
317
321
  else
318
- @api.namespace_stackable(:declared_params, @declared_params)
322
+ @api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
319
323
  end
320
-
321
- # params were stored in settings, it can be cleaned from the params scope
324
+ ensure
322
325
  @declared_params = nil
323
326
  end
324
327
 
325
- def validates(attrs, validations)
326
- doc = AttributesDoc.new @api, self
327
- doc.extract_details validations
328
+ def find_nearest_array_ancestor
329
+ scope = @parent
330
+ scope = scope.parent while scope && scope.type != Array
331
+ scope
332
+ end
328
333
 
334
+ def validates(attrs, validations)
329
335
  coerce_type = infer_coercion(validations)
330
-
331
- doc.type = coerce_type
332
-
336
+ required = validations.key?(:presence)
333
337
  default = validations[:default]
334
-
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]
341
- end
342
-
343
- doc.values = values
344
-
345
- except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
338
+ values = validations[:values].is_a?(Hash) ? validations.dig(:values, :value) : validations[:values]
339
+ except_values = validations[:except_values].is_a?(Hash) ? validations.dig(:except_values, :value) : validations[:except_values]
346
340
 
347
341
  # NB. values and excepts should be nil, Proc, Array, or Range.
348
342
  # Specifically, values should NOT be a Hash
349
-
350
343
  # 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)
344
+ coerce_type = guess_coerce_type(coerce_type, values, except_values)
352
345
 
353
346
  # 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)
347
+ check_incompatible_option_values(default, values, except_values)
355
348
 
356
349
  # type should be compatible with values array, if both exist
357
- validate_value_coercion(coerce_type, values, except_values, excepts)
350
+ validate_value_coercion(coerce_type, values, except_values)
358
351
 
359
- doc.document attrs
352
+ document_params attrs, validations, coerce_type, values, except_values
360
353
 
361
- opts = derive_validator_options(validations)
354
+ opts = derive_validator_options(validations).freeze
362
355
 
363
356
  # Validate for presence before any other validators
364
- validates_presence(validations, attrs, doc, opts)
357
+ validates_presence(validations, attrs, opts)
365
358
 
366
359
  # Before we run the rest of the validators, let's handle
367
360
  # whatever coercion so that we are working with correctly
368
361
  # type casted values
369
- coerce_type validations, attrs, doc, opts
362
+ coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts
370
363
 
371
364
  validations.each do |type, options|
372
365
  # Don't try to look up validators for documentation params that don't have one.
373
366
  next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
374
367
 
375
- validate(type, options, attrs, doc, opts)
368
+ validate(type, options, attrs, required, opts)
376
369
  end
377
370
  end
378
371
 
@@ -425,7 +418,7 @@ module Grape
425
418
 
426
419
  # but not special JSON types, which
427
420
  # already imply coercion method
428
- return if [JSON, Array[JSON]].exclude? validations[:coerce]
421
+ return unless SPECIAL_JSON.include?(validations[:coerce])
429
422
 
430
423
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
431
424
  end
@@ -436,20 +429,22 @@ module Grape
436
429
  # composited from more than one +requires+/+optional+
437
430
  # parameter, and needs to be run before most other
438
431
  # validations.
439
- def coerce_type(validations, attrs, doc, opts)
432
+ def coerce_type(validations, attrs, required, opts)
440
433
  check_coerce_with(validations)
441
434
 
442
- return unless validations.key?(:coerce)
435
+ # Falsy check (not key?) is intentional: when a remountable API is first
436
+ # evaluated on its base instance (no configuration supplied yet),
437
+ # configuration[:some_type] evaluates to nil. Skipping instantiation
438
+ # here is correct — the real mounted instance will replay this step with
439
+ # the actual type value.
440
+ return unless validations[:coerce]
443
441
 
444
442
  coerce_options = {
445
443
  type: validations[:coerce],
446
444
  method: validations[:coerce_with],
447
445
  message: validations[:coerce_message]
448
446
  }
449
- validate('coerce', coerce_options, attrs, doc, opts)
450
- validations.delete(:coerce_with)
451
- validations.delete(:coerce)
452
- validations.delete(:coerce_message)
447
+ validate('coerce', coerce_options, attrs, required, opts)
453
448
  end
454
449
 
455
450
  def guess_coerce_type(coerce_type, *values_list)
@@ -462,30 +457,26 @@ module Grape
462
457
  coerce_type
463
458
  end
464
459
 
465
- def check_incompatible_option_values(default, values, except_values, excepts)
460
+ def check_incompatible_option_values(default, values, except_values)
466
461
  return unless default && !default.is_a?(Proc)
467
462
 
468
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) }
469
464
 
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
465
+ return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
473
466
 
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) }
467
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
477
468
  end
478
469
 
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)
470
+ def validate(type, options, attrs, required, opts)
471
+ validator_class = Validations.require_validator(type)
472
+ validator_instance = validator_class.new(
473
+ attrs,
474
+ options,
475
+ required,
476
+ self,
477
+ opts
478
+ )
479
+ @api.inheritable_setting.namespace_stackable[:validations] = validator_instance
489
480
  end
490
481
 
491
482
  def validate_value_coercion(coerce_type, *values_list)
@@ -527,10 +518,10 @@ module Grape
527
518
  }
528
519
  end
529
520
 
530
- def validates_presence(validations, attrs, doc, opts)
521
+ def validates_presence(validations, attrs, opts)
531
522
  return unless validations.key?(:presence) && validations[:presence]
532
523
 
533
- validate('presence', validations.delete(:presence), attrs, doc, opts)
524
+ validate('presence', validations.delete(:presence), attrs, true, opts)
534
525
  validations.delete(:message) if validations.key?(:message)
535
526
  end
536
527
  end
@@ -5,10 +5,10 @@ module Grape
5
5
  class SingleAttributeIterator < AttributesIterator
6
6
  private
7
7
 
8
- def yield_attributes(val, attrs)
8
+ def yield_attributes(val)
9
9
  return if skip?(val)
10
10
 
11
- attrs.each do |attr_name|
11
+ @attrs.each do |attr_name|
12
12
  yield val, attr_name, empty?(val)
13
13
  end
14
14
  end
@@ -7,15 +7,14 @@ module Grape
7
7
  # an array of arrays of integers.
8
8
  #
9
9
  # It could've been possible to use an +of+
10
- # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
10
+ # method (https://dry-rb.org/gems/dry-types/main/array-with-member/)
11
11
  # provided by dry-types. Unfortunately, it doesn't work for Grape because of
12
12
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
13
13
  # maintains Virtus behavior in coercing.
14
14
  class ArrayCoercer < DryTypeCoercer
15
15
  def initialize(type, strict = false)
16
16
  super
17
-
18
- @coercer = scope::Array
17
+ @coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
19
18
  @subtype = type.first
20
19
  end
21
20
 
@@ -1,19 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DryTypes
4
- # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
5
- # a container in this case. Check documentation for more information
6
- # https://dry-rb.org/gems/dry-types/1.2/getting-started/
7
- include Dry.Types()
8
- end
9
-
10
3
  module Grape
11
4
  module Validations
12
5
  module Types
13
6
  # A base class for classes which must identify a coercer to be used.
14
7
  # If the +strict+ argument is true, it won't coerce the given value
15
8
  # but check its type. More information there
16
- # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
9
+ # https://dry-rb.org/gems/dry-types/main/built-in-types/
17
10
  class DryTypeCoercer
18
11
  class << self
19
12
  # Returns a collection coercer which corresponds to a given type.
@@ -42,7 +35,7 @@ module Grape
42
35
  def initialize(type, strict = false)
43
36
  @type = type
44
37
  @strict = strict
45
- @scope = strict ? DryTypes::Strict : DryTypes::Params
38
+ @cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
46
39
  end
47
40
 
48
41
  # Coerces the given value to a type which was specified during
@@ -53,13 +46,13 @@ module Grape
53
46
  return if val.nil?
54
47
 
55
48
  @coercer[val]
56
- rescue Dry::Types::CoercionError => _e
49
+ rescue Dry::Types::CoercionError
57
50
  InvalidValue.new
58
51
  end
59
52
 
60
53
  protected
61
54
 
62
- attr_reader :scope, :type, :strict
55
+ attr_reader :type, :strict, :cache_coercer
63
56
  end
64
57
  end
65
58
  end
@@ -7,37 +7,10 @@ module Grape
7
7
  # initialization. When +strict+ is true, it doesn't coerce a value but check
8
8
  # that it has the proper type.
9
9
  class PrimitiveCoercer < DryTypeCoercer
10
- MAPPING = {
11
- Grape::API::Boolean => DryTypes::Params::Bool,
12
- BigDecimal => DryTypes::Params::Decimal,
13
- Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
14
- TrueClass => DryTypes::Params::Bool.constrained(eql: true),
15
- FalseClass => DryTypes::Params::Bool.constrained(eql: false),
16
-
17
- # unfortunately, a +Params+ scope doesn't contain String
18
- String => DryTypes::Coercible::String
19
- }.freeze
20
-
21
- STRICT_MAPPING = {
22
- Grape::API::Boolean => DryTypes::Strict::Bool,
23
- BigDecimal => DryTypes::Strict::Decimal,
24
- Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
25
- TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
26
- FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
27
- }.freeze
28
-
29
10
  def initialize(type, strict = false)
30
11
  super
31
12
 
32
- @type = type
33
-
34
- @coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
35
- scope.const_get(type.name, false)
36
- rescue NameError
37
- raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
38
-
39
- type
40
- end
13
+ @coercer = cache_coercer[type]
41
14
  end
42
15
 
43
16
  def call(val)