grape 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -2
  3. data/README.md +43 -10
  4. data/UPGRADING.md +91 -0
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +13 -17
  7. data/lib/grape/api.rb +7 -14
  8. data/lib/grape/cookies.rb +2 -0
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/desc.rb +3 -5
  11. data/lib/grape/dsl/helpers.rb +6 -4
  12. data/lib/grape/dsl/inside_route.rb +18 -9
  13. data/lib/grape/dsl/middleware.rb +4 -4
  14. data/lib/grape/dsl/parameters.rb +11 -7
  15. data/lib/grape/dsl/request_response.rb +9 -6
  16. data/lib/grape/dsl/routing.rb +7 -6
  17. data/lib/grape/dsl/settings.rb +5 -5
  18. data/lib/grape/endpoint.rb +21 -36
  19. data/lib/grape/error_formatter/json.rb +2 -6
  20. data/lib/grape/error_formatter/xml.rb +2 -6
  21. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  22. data/lib/grape/exceptions/validation.rb +2 -3
  23. data/lib/grape/exceptions/validation_errors.rb +1 -1
  24. data/lib/grape/formatter/json.rb +1 -0
  25. data/lib/grape/formatter/serializable_hash.rb +2 -1
  26. data/lib/grape/formatter/xml.rb +1 -0
  27. data/lib/grape/locale/en.yml +1 -1
  28. data/lib/grape/middleware/auth/base.rb +3 -3
  29. data/lib/grape/middleware/base.rb +4 -2
  30. data/lib/grape/middleware/error.rb +1 -1
  31. data/lib/grape/middleware/formatter.rb +4 -4
  32. data/lib/grape/middleware/stack.rb +10 -16
  33. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
  34. data/lib/grape/middleware/versioner/header.rb +6 -4
  35. data/lib/grape/middleware/versioner/param.rb +1 -0
  36. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  37. data/lib/grape/middleware/versioner/path.rb +2 -0
  38. data/lib/grape/parser/json.rb +1 -1
  39. data/lib/grape/parser/xml.rb +1 -1
  40. data/lib/grape/path.rb +1 -0
  41. data/lib/grape/request.rb +3 -0
  42. data/lib/grape/router/attribute_translator.rb +1 -1
  43. data/lib/grape/router/pattern.rb +1 -1
  44. data/lib/grape/router/route.rb +2 -2
  45. data/lib/grape/router.rb +6 -0
  46. data/lib/grape/util/inheritable_setting.rb +1 -3
  47. data/lib/grape/util/lazy_value.rb +3 -2
  48. data/lib/grape/validations/attributes_iterator.rb +8 -0
  49. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  50. data/lib/grape/validations/params_scope.rb +92 -58
  51. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  52. data/lib/grape/validations/types/custom_type_coercer.rb +3 -2
  53. data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
  54. data/lib/grape/validations/types/invalid_value.rb +24 -0
  55. data/lib/grape/validations/types/json.rb +2 -1
  56. data/lib/grape/validations/types/primitive_coercer.rb +3 -3
  57. data/lib/grape/validations/types.rb +1 -4
  58. data/lib/grape/validations/validator_factory.rb +1 -1
  59. data/lib/grape/validations/validators/all_or_none.rb +1 -0
  60. data/lib/grape/validations/validators/as.rb +4 -8
  61. data/lib/grape/validations/validators/at_least_one_of.rb +1 -0
  62. data/lib/grape/validations/validators/base.rb +12 -7
  63. data/lib/grape/validations/validators/coerce.rb +8 -9
  64. data/lib/grape/validations/validators/default.rb +1 -0
  65. data/lib/grape/validations/validators/exactly_one_of.rb +1 -0
  66. data/lib/grape/validations/validators/multiple_params_base.rb +5 -2
  67. data/lib/grape/validations/validators/mutual_exclusion.rb +1 -0
  68. data/lib/grape/validations/validators/presence.rb +1 -0
  69. data/lib/grape/validations/validators/regexp.rb +1 -0
  70. data/lib/grape/validations/validators/same_as.rb +1 -0
  71. data/lib/grape/validations/validators/values.rb +3 -0
  72. data/lib/grape/version.rb +1 -1
  73. data/lib/grape.rb +3 -1
  74. data/spec/grape/api/custom_validations_spec.rb +1 -0
  75. data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
  76. data/spec/grape/api_remount_spec.rb +9 -4
  77. data/spec/grape/api_spec.rb +203 -37
  78. data/spec/grape/dsl/callbacks_spec.rb +1 -1
  79. data/spec/grape/dsl/middleware_spec.rb +1 -1
  80. data/spec/grape/dsl/parameters_spec.rb +1 -0
  81. data/spec/grape/dsl/routing_spec.rb +1 -1
  82. data/spec/grape/endpoint/declared_spec.rb +259 -1
  83. data/spec/grape/endpoint_spec.rb +18 -5
  84. data/spec/grape/entity_spec.rb +10 -10
  85. data/spec/grape/middleware/auth/dsl_spec.rb +1 -1
  86. data/spec/grape/middleware/error_spec.rb +1 -2
  87. data/spec/grape/middleware/formatter_spec.rb +2 -2
  88. data/spec/grape/middleware/stack_spec.rb +4 -3
  89. data/spec/grape/request_spec.rb +1 -1
  90. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  91. data/spec/grape/validations/params_scope_spec.rb +37 -3
  92. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  93. data/spec/grape/validations/types/primitive_coercer_spec.rb +2 -2
  94. data/spec/grape/validations/validators/coerce_spec.rb +129 -22
  95. data/spec/grape/validations/validators/except_values_spec.rb +2 -2
  96. data/spec/grape/validations/validators/values_spec.rb +15 -11
  97. data/spec/grape/validations_spec.rb +280 -0
  98. data/spec/shared/versioning_examples.rb +22 -22
  99. data/spec/spec_helper.rb +1 -1
  100. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  101. data/spec/support/versioned_helpers.rb +1 -1
  102. metadata +8 -6
@@ -13,6 +13,8 @@ module Grape
13
13
  # @param opts [Hash] options for this scope
14
14
  # @option opts :element [Symbol] the element that contains this scope; for
15
15
  # this to be relevant, @parent must be set
16
+ # @option opts :element_renamed [Symbol, nil] whenever this scope should
17
+ # be renamed and to what, given +nil+ no renaming is done
16
18
  # @option opts :parent [ParamsScope] the scope containing this scope
17
19
  # @option opts :api [API] the API endpoint to modify
18
20
  # @option opts :optional [Boolean] whether or not this scope needs to have
@@ -23,23 +25,24 @@ module Grape
23
25
  # validate if this param is present in the parent scope
24
26
  # @yield the instance context, open for parameter definitions
25
27
  def initialize(opts, &block)
26
- @element = opts[:element]
27
- @parent = opts[:parent]
28
- @api = opts[:api]
29
- @optional = opts[:optional] || false
30
- @type = opts[:type]
31
- @group = opts[:group] || {}
32
- @dependent_on = opts[:dependent_on]
28
+ @element = opts[:element]
29
+ @element_renamed = opts[:element_renamed]
30
+ @parent = opts[:parent]
31
+ @api = opts[:api]
32
+ @optional = opts[:optional] || false
33
+ @type = opts[:type]
34
+ @group = opts[:group] || {}
35
+ @dependent_on = opts[:dependent_on]
33
36
  @declared_params = []
34
37
  @index = nil
35
38
 
36
- instance_eval(&block) if block_given?
39
+ instance_eval(&block) if block
37
40
 
38
41
  configure_declared_params
39
42
  end
40
43
 
41
44
  def configuration
42
- @api.configuration.evaluate
45
+ @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
43
46
  end
44
47
 
45
48
  # @return [Boolean] whether or not this entire scope needs to be
@@ -50,19 +53,19 @@ module Grape
50
53
  return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
51
54
  return false unless meets_dependency?(scoped_params, parameters)
52
55
  return true if parent.nil?
56
+
53
57
  parent.should_validate?(parameters)
54
58
  end
55
59
 
56
60
  def meets_dependency?(params, request_params)
57
61
  return true unless @dependent_on
58
62
 
59
- if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
60
- return false
61
- end
63
+ return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
62
64
 
63
65
  return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
64
- return false unless params.respond_to?(:with_indifferent_access)
65
- params = params.with_indifferent_access
66
+
67
+ # params might be anything what looks like a hash, so it must implement a `key?` method
68
+ return false unless params.respond_to?(:key?)
66
69
 
67
70
  @dependent_on.each do |dependency|
68
71
  if dependency.is_a?(Hash)
@@ -128,18 +131,35 @@ module Grape
128
131
  if lateral?
129
132
  @parent.push_declared_params(attrs, **opts)
130
133
  else
131
- if opts && opts[:as]
132
- @api.route_setting(:renamed_params, @api.route_setting(:renamed_params) || [])
133
- @api.route_setting(:renamed_params) << { attrs.first => opts[:as] }
134
- attrs = [opts[:as]]
135
- end
134
+ push_renamed_param(full_path + [attrs.first], opts[:as]) \
135
+ if opts && opts[:as]
136
136
 
137
137
  @declared_params.concat attrs
138
138
  end
139
139
  end
140
140
 
141
+ # Get the full path of the parameter scope in the hierarchy.
142
+ #
143
+ # @return [Array<Symbol>] the nesting/path of the current parameter scope
144
+ def full_path
145
+ nested? ? @parent.full_path + [@element] : []
146
+ end
147
+
141
148
  private
142
149
 
150
+ # Add a new parameter which should be renamed when using the +#declared+
151
+ # method.
152
+ #
153
+ # @param path [Array<String, Symbol>] the full path of the parameter
154
+ # (including the parameter name as last array element)
155
+ # @param new_name [String, Symbol] the new name of the parameter (the
156
+ # renamed name, with the +as: ...+ semantic)
157
+ def push_renamed_param(path, new_name)
158
+ base = @api.route_setting(:renamed_params) || {}
159
+ base[Array(path).map(&:to_s)] = new_name.to_s
160
+ @api.route_setting(:renamed_params, base)
161
+ end
162
+
143
163
  def require_required_and_optional_fields(context, opts)
144
164
  if context == :all
145
165
  optional_fields = Array(opts[:except])
@@ -151,6 +171,7 @@ module Grape
151
171
  required_fields.each do |field|
152
172
  field_opts = opts[:using][field]
153
173
  raise ArgumentError, "required field not exist: #{field}" unless field_opts
174
+
154
175
  requires(field, field_opts)
155
176
  end
156
177
  optional_fields.each do |field|
@@ -190,11 +211,12 @@ module Grape
190
211
  end
191
212
 
192
213
  self.class.new(
193
- api: @api,
194
- element: attrs[1][:as] || attrs.first,
195
- parent: self,
214
+ api: @api,
215
+ element: attrs.first,
216
+ element_renamed: attrs[1][:as],
217
+ parent: self,
196
218
  optional: optional,
197
- type: type || Array,
219
+ type: type || Array,
198
220
  &block
199
221
  )
200
222
  end
@@ -208,11 +230,11 @@ module Grape
208
230
  # @yield parameter scope
209
231
  def new_lateral_scope(options, &block)
210
232
  self.class.new(
211
- api: @api,
212
- element: nil,
213
- parent: self,
214
- options: @optional,
215
- type: type == Array ? Array : Hash,
233
+ api: @api,
234
+ element: nil,
235
+ parent: self,
236
+ options: @optional,
237
+ type: type == Array ? Array : Hash,
216
238
  dependent_on: options[:dependent_on],
217
239
  &block
218
240
  )
@@ -225,15 +247,17 @@ module Grape
225
247
  # @yield parameter scope
226
248
  def new_group_scope(attrs, &block)
227
249
  self.class.new(
228
- api: @api,
229
- parent: self,
230
- group: attrs.first,
250
+ api: @api,
251
+ parent: self,
252
+ group: attrs.first,
231
253
  &block
232
254
  )
233
255
  end
234
256
 
235
257
  # Pushes declared params to parent or settings
236
258
  def configure_declared_params
259
+ push_renamed_param(full_path, @element_renamed) if @element_renamed
260
+
237
261
  if nested?
238
262
  @parent.push_declared_params [element => @declared_params]
239
263
  else
@@ -282,16 +306,15 @@ module Grape
282
306
 
283
307
  doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
284
308
 
285
- full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
286
- @api.document_attribute(full_attrs, doc_attrs)
309
+ document_attribute(attrs, doc_attrs)
287
310
 
288
311
  opts = derive_validator_options(validations)
289
312
 
313
+ order_specific_validations = Set[:as]
314
+
290
315
  # Validate for presence before any other validators
291
- if validations.key?(:presence) && validations[:presence]
292
- validate('presence', validations[:presence], attrs, doc_attrs, opts)
293
- validations.delete(:presence)
294
- validations.delete(:message) if validations.key?(:message)
316
+ validates_presence(validations, attrs, doc_attrs, opts) do |validation_type|
317
+ order_specific_validations << validation_type
295
318
  end
296
319
 
297
320
  # Before we run the rest of the validators, let's handle
@@ -300,6 +323,8 @@ module Grape
300
323
  coerce_type validations, attrs, doc_attrs, opts
301
324
 
302
325
  validations.each do |type, options|
326
+ next if order_specific_validations.include?(type)
327
+
303
328
  validate(type, options, attrs, doc_attrs, opts)
304
329
  end
305
330
  end
@@ -318,9 +343,7 @@ module Grape
318
343
  # @return [class-like] type to which the parameter will be coerced
319
344
  # @raise [ArgumentError] if the given type options are invalid
320
345
  def infer_coercion(validations)
321
- if validations.key?(:type) && validations.key?(:types)
322
- raise ArgumentError, ':type may not be supplied with :types'
323
- end
346
+ raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
324
347
 
325
348
  validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
326
349
  validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
@@ -356,6 +379,7 @@ module Grape
356
379
  # but not special JSON types, which
357
380
  # already imply coercion method
358
381
  return unless [JSON, Array[JSON]].include? validations[:coerce]
382
+
359
383
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
360
384
  end
361
385
 
@@ -383,6 +407,7 @@ module Grape
383
407
 
384
408
  def guess_coerce_type(coerce_type, *values_list)
385
409
  return coerce_type unless coerce_type == Array
410
+
386
411
  values_list.each do |values|
387
412
  next if !values || values.is_a?(Proc)
388
413
  return values.first.class if values.is_a?(Range) || !values.empty?
@@ -393,14 +418,11 @@ module Grape
393
418
  def check_incompatible_option_values(default, values, except_values, excepts)
394
419
  return unless default && !default.is_a?(Proc)
395
420
 
396
- if values && !values.is_a?(Proc)
397
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \
398
- unless Array(default).all? { |def_val| values.include?(def_val) }
399
- end
421
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
400
422
 
401
- if except_values && !except_values.is_a?(Proc)
423
+ if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
402
424
  raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \
403
- unless Array(default).none? { |def_val| except_values.include?(def_val) }
425
+
404
426
  end
405
427
 
406
428
  return unless excepts && !excepts.is_a?(Proc)
@@ -414,11 +436,11 @@ module Grape
414
436
  raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
415
437
 
416
438
  validator_options = {
417
- attributes: attrs,
418
- options: options,
419
- required: doc_attrs[:required],
420
- params_scope: self,
421
- opts: opts,
439
+ attributes: attrs,
440
+ options: options,
441
+ required: doc_attrs[:required],
442
+ params_scope: self,
443
+ opts: opts,
422
444
  validator_class: validator_class
423
445
  }
424
446
  @api.namespace_stackable(:validations, validator_options)
@@ -426,21 +448,20 @@ module Grape
426
448
 
427
449
  def validate_value_coercion(coerce_type, *values_list)
428
450
  return unless coerce_type
451
+
429
452
  coerce_type = coerce_type.first if coerce_type.is_a?(Array)
430
453
  values_list.each do |values|
431
454
  next if !values || values.is_a?(Proc)
455
+
432
456
  value_types = values.is_a?(Range) ? [values.begin, values.end] : values
433
- if coerce_type == Grape::API::Boolean
434
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) }
435
- end
436
- unless value_types.all? { |v| v.is_a? coerce_type }
437
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
438
- end
457
+ value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
458
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
439
459
  end
440
460
  end
441
461
 
442
462
  def extract_message_option(attrs)
443
463
  return nil unless attrs.is_a?(Array)
464
+
444
465
  opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
445
466
  opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
446
467
  end
@@ -460,9 +481,22 @@ module Grape
460
481
 
461
482
  {
462
483
  allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
463
- fail_fast: validations.delete(:fail_fast) || false
484
+ fail_fast: validations.delete(:fail_fast) || false
464
485
  }
465
486
  end
487
+
488
+ def validates_presence(validations, attrs, doc_attrs, opts)
489
+ return unless validations.key?(:presence) && validations[:presence]
490
+
491
+ validate(:presence, validations[:presence], attrs, doc_attrs, opts)
492
+ yield :presence
493
+ yield :message if validations.key?(:message)
494
+ end
495
+
496
+ def document_attribute(attrs, doc_attrs)
497
+ full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
498
+ @api.document_attribute(full_attrs, doc_attrs)
499
+ end
466
500
  end
467
501
  end
468
502
  end
@@ -7,7 +7,7 @@ module Grape
7
7
 
8
8
  def yield_attributes(val, attrs)
9
9
  attrs.each do |attr_name|
10
- yield val, attr_name, empty?(val)
10
+ yield val, attr_name, empty?(val), skip?(val)
11
11
  end
12
12
  end
13
13
 
@@ -52,10 +52,11 @@ module Grape
52
52
  # this should always be a string.
53
53
  # @return [Object] the coerced result
54
54
  def call(val)
55
- return if val.nil?
56
-
57
55
  coerced_val = @method.call(val)
56
+
57
+ return coerced_val if coerced_val.is_a?(InvalidValue)
58
58
  return InvalidValue.new unless coerced?(coerced_val)
59
+
59
60
  coerced_val
60
61
  end
61
62
 
@@ -35,7 +35,7 @@ module Grape
35
35
 
36
36
  # Returns an instance of a coercer for a given type
37
37
  def coercer_instance_for(type, strict = false)
38
- return PrimitiveCoercer.new(type, strict) if type.class == Class
38
+ return PrimitiveCoercer.new(type, strict) if type.instance_of?(Class)
39
39
 
40
40
  # in case of a collection (Array[Integer]) the type is an instance of a collection,
41
41
  # so we need to figure out the actual type
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Instances of this class may be used as tokens to denote that a parameter value could not be
7
+ # coerced. The given message will be used as a validation error.
8
+ class InvalidValue
9
+ attr_reader :message
10
+
11
+ def initialize(message = nil)
12
+ @message = message
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # only exists to make it shorter for external use
20
+ module Grape
21
+ module Types
22
+ InvalidValue = Class.new(Grape::Validations::Types::InvalidValue)
23
+ end
24
+ end
@@ -22,6 +22,7 @@ module Grape
22
22
 
23
23
  # Allow nulls and blank strings
24
24
  return if input.nil? || input.match?(/^\s*$/)
25
+
25
26
  JSON.parse(input, symbolize_names: true)
26
27
  end
27
28
 
@@ -41,7 +42,7 @@ module Grape
41
42
  # @param value [Object] result of {#parse}
42
43
  # @return [true,false]
43
44
  def coerced_collection?(value)
44
- value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
45
+ value.is_a?(::Array) && value.all?(::Hash)
45
46
  end
46
47
  end
47
48
  end
@@ -11,15 +11,15 @@ module Grape
11
11
  class PrimitiveCoercer < DryTypeCoercer
12
12
  MAPPING = {
13
13
  Grape::API::Boolean => DryTypes::Params::Bool,
14
- BigDecimal => DryTypes::Params::Decimal,
14
+ BigDecimal => DryTypes::Params::Decimal,
15
15
 
16
16
  # unfortunately, a +Params+ scope doesn't contain String
17
- String => DryTypes::Coercible::String
17
+ String => DryTypes::Coercible::String
18
18
  }.freeze
19
19
 
20
20
  STRICT_MAPPING = {
21
21
  Grape::API::Boolean => DryTypes::Strict::Bool,
22
- BigDecimal => DryTypes::Strict::Decimal
22
+ BigDecimal => DryTypes::Strict::Decimal
23
23
  }.freeze
24
24
 
25
25
  def initialize(type, strict = false)
@@ -7,6 +7,7 @@ require_relative 'types/multiple_type_coercer'
7
7
  require_relative 'types/variant_collection_coercer'
8
8
  require_relative 'types/json'
9
9
  require_relative 'types/file'
10
+ require_relative 'types/invalid_value'
10
11
 
11
12
  module Grape
12
13
  module Validations
@@ -21,10 +22,6 @@ module Grape
21
22
  # and {Grape::Dsl::Parameters#optional}. The main
22
23
  # entry point for this process is {Types.build_coercer}.
23
24
  module Types
24
- # Instances of this class may be used as tokens to denote that
25
- # a parameter value could not be coerced.
26
- class InvalidValue; end
27
-
28
25
  # Types representing a single value, which are coerced.
29
26
  PRIMITIVES = [
30
27
  # Numerical
@@ -8,7 +8,7 @@ module Grape
8
8
  options[:options],
9
9
  options[:required],
10
10
  options[:params_scope],
11
- options[:opts])
11
+ **options[:opts])
12
12
  end
13
13
  end
14
14
  end
@@ -8,6 +8,7 @@ module Grape
8
8
  def validate_params!(params)
9
9
  keys = keys_in_common(params)
10
10
  return if keys.empty? || keys.length == all_keys.length
11
+
11
12
  raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none))
12
13
  end
13
14
  end
@@ -3,14 +3,10 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AsValidator < Base
6
- def initialize(attrs, options, required, scope, **opts)
7
- @renamed_options = options
8
- super
9
- end
10
-
11
- def validate_param!(attr_name, params)
12
- params[@renamed_options] = params[attr_name]
13
- end
6
+ # We use a validator for renaming parameters. This is just a marker for
7
+ # the parameter scope to handle the renaming. No actual validation
8
+ # happens here.
9
+ def validate_param!(*); end
14
10
  end
15
11
  end
16
12
  end
@@ -7,6 +7,7 @@ module Grape
7
7
  class AtLeastOneOfValidator < MultipleParamsBase
8
8
  def validate_params!(params)
9
9
  return unless keys_in_common(params).empty?
10
+
10
11
  raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one))
11
12
  end
12
13
  end
@@ -12,14 +12,15 @@ module Grape
12
12
  # @param options [Object] implementation-dependent Validator options
13
13
  # @param required [Boolean] attribute(s) are required or optional
14
14
  # @param scope [ParamsScope] parent scope for this Validator
15
- # @param opts [Hash] additional validation options
16
- def initialize(attrs, options, required, scope, **opts)
15
+ # @param opts [Array] additional validation options
16
+ def initialize(attrs, options, required, scope, *opts)
17
17
  @attrs = Array(attrs)
18
18
  @option = options
19
19
  @required = required
20
20
  @scope = scope
21
- @fail_fast = opts[:fail_fast] || false
22
- @allow_blank = opts[:allow_blank] || false
21
+ opts = opts.any? ? opts.shift : {}
22
+ @fail_fast = opts.fetch(:fail_fast, false)
23
+ @allow_blank = opts.fetch(:allow_blank, false)
23
24
  end
24
25
 
25
26
  # Validates a given request.
@@ -29,6 +30,7 @@ module Grape
29
30
  # @return [void]
30
31
  def validate(request)
31
32
  return unless @scope.should_validate?(request.params)
33
+
32
34
  validate!(request.params)
33
35
  end
34
36
 
@@ -43,17 +45,19 @@ module Grape
43
45
  # there may be more than one error per field
44
46
  array_errors = []
45
47
 
46
- attributes.each do |val, attr_name, empty_val|
48
+ attributes.each do |val, attr_name, empty_val, skip_value|
49
+ next if skip_value
47
50
  next if !@scope.required? && empty_val
48
51
  next unless @scope.meets_dependency?(val, params)
52
+
49
53
  begin
50
- validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name)
54
+ validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name))
51
55
  rescue Grape::Exceptions::Validation => e
52
56
  array_errors << e
53
57
  end
54
58
  end
55
59
 
56
- raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any?
60
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
57
61
  end
58
62
 
59
63
  def self.convert_to_short_name(klass)
@@ -67,6 +71,7 @@ module Grape
67
71
 
68
72
  def self.inherited(klass)
69
73
  return unless klass.name.present?
74
+
70
75
  Validations.register_validator(convert_to_short_name(klass), klass)
71
76
  end
72
77
 
@@ -17,7 +17,7 @@ module Grape
17
17
 
18
18
  module Validations
19
19
  class CoerceValidator < Base
20
- def initialize(*_args)
20
+ def initialize(attrs, options, required, scope, **opts)
21
21
  super
22
22
 
23
23
  @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
@@ -27,16 +27,12 @@ module Grape
27
27
  end
28
28
  end
29
29
 
30
- def validate(request)
31
- super
32
- end
33
-
34
30
  def validate_param!(attr_name, params)
35
31
  raise validation_exception(attr_name) unless params.is_a? Hash
36
32
 
37
33
  new_value = coerce_value(params[attr_name])
38
34
 
39
- raise validation_exception(attr_name) unless valid_type?(new_value)
35
+ raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value)
40
36
 
41
37
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
42
38
  # which looses wrappers for hashes and arrays after reassigning values
@@ -47,7 +43,7 @@ module Grape
47
43
  # h[:list] = list
48
44
  # h
49
45
  # => #<Hashie::Mash list=[1, 2, 3, 4]>
50
- return if params[attr_name].class == new_value.class && params[attr_name] == new_value
46
+ return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value
51
47
 
52
48
  params[attr_name] = new_value
53
49
  end
@@ -80,8 +76,11 @@ module Grape
80
76
  @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type]
81
77
  end
82
78
 
83
- def validation_exception(attr_name)
84
- Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:coerce))
79
+ def validation_exception(attr_name, custom_msg = nil)
80
+ Grape::Exceptions::Validation.new(
81
+ params: [@scope.full_name(attr_name)],
82
+ message: custom_msg || message(:coerce)
83
+ )
85
84
  end
86
85
  end
87
86
  end
@@ -22,6 +22,7 @@ module Grape
22
22
  attrs = SingleAttributeIterator.new(self, @scope, params)
23
23
  attrs.each do |resource_params, attr_name|
24
24
  next unless @scope.meets_dependency?(resource_params, params)
25
+
25
26
  validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
26
27
  end
27
28
  end
@@ -9,6 +9,7 @@ module Grape
9
9
  keys = keys_in_common(params)
10
10
  return if keys.length == 1
11
11
  raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero?
12
+
12
13
  raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
13
14
  end
14
15
  end
@@ -7,7 +7,9 @@ module Grape
7
7
  attributes = MultipleAttributesIterator.new(self, @scope, params)
8
8
  array_errors = []
9
9
 
10
- attributes.each do |resource_params|
10
+ attributes.each do |resource_params, skip_value|
11
+ next if skip_value
12
+
11
13
  begin
12
14
  validate_params!(resource_params)
13
15
  rescue Grape::Exceptions::Validation => e
@@ -15,13 +17,14 @@ module Grape
15
17
  end
16
18
  end
17
19
 
18
- raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any?
20
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
19
21
  end
20
22
 
21
23
  private
22
24
 
23
25
  def keys_in_common(resource_params)
24
26
  return [] unless resource_params.is_a?(Hash)
27
+
25
28
  all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) }
26
29
  end
27
30
 
@@ -8,6 +8,7 @@ module Grape
8
8
  def validate_params!(params)
9
9
  keys = keys_in_common(params)
10
10
  return if keys.length <= 1
11
+
11
12
  raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
12
13
  end
13
14
  end
@@ -5,6 +5,7 @@ module Grape
5
5
  class PresenceValidator < Base
6
6
  def validate_param!(attr_name, params)
7
7
  return if params.respond_to?(:key?) && params.key?(attr_name)
8
+
8
9
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence))
9
10
  end
10
11
  end
@@ -6,6 +6,7 @@ module Grape
6
6
  def validate_param!(attr_name, params)
7
7
  return unless params.respond_to?(:key?) && params.key?(attr_name)
8
8
  return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) }
9
+
9
10
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
10
11
  end
11
12
  end
@@ -6,6 +6,7 @@ module Grape
6
6
  def validate_param!(attr_name, params)
7
7
  confirmation = options_key?(:value) ? @option[:value] : @option
8
8
  return if params[attr_name] == params[confirmation]
9
+
9
10
  raise Grape::Exceptions::Validation.new(
10
11
  params: [@scope.full_name(attr_name)],
11
12
  message: build_message