grape 3.1.0 → 3.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +76 -161
- data/UPGRADING.md +106 -0
- data/grape.gemspec +2 -2
- data/lib/grape/api/instance.rb +1 -1
- data/lib/grape/api.rb +1 -1
- data/lib/grape/declared_params_handler.rb +3 -3
- data/lib/grape/dsl/declared.rb +1 -1
- data/lib/grape/dsl/desc.rb +1 -1
- data/lib/grape/dsl/inside_route.rb +9 -9
- data/lib/grape/dsl/parameters.rb +14 -14
- data/lib/grape/dsl/routing.rb +8 -8
- data/lib/grape/endpoint.rb +45 -50
- data/lib/grape/error_formatter/base.rb +2 -2
- data/lib/grape/exceptions/base.rb +18 -44
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/validation.rb +7 -4
- data/lib/grape/exceptions/validation_errors.rb +13 -7
- data/lib/grape/locale/en.yml +0 -5
- data/lib/grape/middleware/auth/base.rb +2 -0
- data/lib/grape/middleware/base.rb +2 -4
- data/lib/grape/middleware/error.rb +2 -2
- data/lib/grape/middleware/formatter.rb +1 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/request.rb +2 -10
- data/lib/grape/router/pattern.rb +1 -1
- data/lib/grape/router.rb +4 -2
- data/lib/grape/util/api_description.rb +1 -1
- data/lib/grape/util/deep_freeze.rb +35 -0
- data/lib/grape/util/inheritable_setting.rb +1 -1
- data/lib/grape/util/media_type.rb +1 -1
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +33 -18
- data/lib/grape/validations/contract_scope.rb +1 -7
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/param_scope_tracker.rb +57 -0
- data/lib/grape/validations/params_scope.rb +111 -107
- data/lib/grape/validations/single_attribute_iterator.rb +2 -2
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +95 -18
- data/lib/grape/validations/validators/coerce_validator.rb +15 -35
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -8
- data/lib/grape/validations/validators/default_validator.rb +12 -18
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +13 -4
- data/lib/grape/validations/validators/length_validator.rb +21 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
- data/lib/grape/validations/validators/mutually_exclusive_validator.rb +3 -1
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +8 -10
- data/lib/grape/validations/validators/same_as_validator.rb +6 -15
- data/lib/grape/validations/validators/values_validator.rb +29 -21
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +18 -1
- metadata +12 -14
- data/lib/grape/exceptions/conflicting_types.rb +0 -11
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/invalid_parameters.rb +0 -11
- data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/validations/validator_factory.rb +0 -15
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
5
|
class ParamsScope
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
10
13
|
include Grape::Validations::ParamsDocumentation
|
|
@@ -17,7 +20,7 @@ module Grape
|
|
|
17
20
|
SPECIAL_JSON = [JSON, Array[JSON]].freeze
|
|
18
21
|
|
|
19
22
|
class Attr
|
|
20
|
-
|
|
23
|
+
attr_reader :key, :scope
|
|
21
24
|
|
|
22
25
|
# Open up a new ParamsScope::Attr
|
|
23
26
|
# @param key [Hash, Symbol] key of attr
|
|
@@ -47,36 +50,39 @@ module Grape
|
|
|
47
50
|
|
|
48
51
|
# Open up a new ParamsScope, allowing parameter definitions per
|
|
49
52
|
# Grape::DSL::Params.
|
|
50
|
-
# @param
|
|
51
|
-
# @
|
|
52
|
-
# this to be relevant,
|
|
53
|
-
# @
|
|
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
|
|
54
57
|
# be renamed and to what, given +nil+ no renaming is done
|
|
55
|
-
# @
|
|
56
|
-
# @
|
|
57
|
-
# @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
|
|
58
60
|
# any parameters set or not
|
|
59
|
-
# @
|
|
60
|
-
# @
|
|
61
|
-
# @
|
|
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
|
|
62
64
|
# validate if this param is present in the parent scope
|
|
63
65
|
# @yield the instance context, open for parameter definitions
|
|
64
|
-
def initialize(
|
|
65
|
-
@element =
|
|
66
|
-
@element_renamed =
|
|
67
|
-
@parent =
|
|
68
|
-
@api =
|
|
69
|
-
@optional =
|
|
70
|
-
@type =
|
|
71
|
-
@group =
|
|
72
|
-
@dependent_on =
|
|
73
|
-
|
|
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.
|
|
74
78
|
@declared_params = []
|
|
75
|
-
@
|
|
79
|
+
@full_path = build_full_path
|
|
76
80
|
|
|
77
81
|
instance_eval(&block) if block
|
|
78
82
|
|
|
79
83
|
configure_declared_params
|
|
84
|
+
@nearest_array_ancestor = find_nearest_array_ancestor
|
|
85
|
+
freeze
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
def configuration
|
|
@@ -90,9 +96,9 @@ module Grape
|
|
|
90
96
|
|
|
91
97
|
return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
|
|
92
98
|
return false unless meets_dependency?(scoped_params, parameters)
|
|
93
|
-
return true if parent.nil?
|
|
99
|
+
return true if @parent.nil?
|
|
94
100
|
|
|
95
|
-
parent.should_validate?(parameters)
|
|
101
|
+
@parent.should_validate?(parameters)
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
def meets_dependency?(params, request_params)
|
|
@@ -100,8 +106,9 @@ module Grape
|
|
|
100
106
|
return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
|
|
101
107
|
|
|
102
108
|
if params.is_a?(Array)
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
|
|
110
|
+
ParamScopeTracker.current&.store_qualifying_params(self, filtered)
|
|
111
|
+
return filtered.present?
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
meets_hash_dependency?(params)
|
|
@@ -118,28 +125,27 @@ module Grape
|
|
|
118
125
|
# params might be anything what looks like a hash, so it must implement a `key?` method
|
|
119
126
|
return false unless params.respond_to?(:key?)
|
|
120
127
|
|
|
121
|
-
@dependent_on.
|
|
128
|
+
@dependent_on.all? do |dependency|
|
|
122
129
|
if dependency.is_a?(Hash)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return false
|
|
130
|
+
key, callable = dependency.first
|
|
131
|
+
callable.call(params[key])
|
|
132
|
+
else
|
|
133
|
+
params[dependency].present?
|
|
128
134
|
end
|
|
129
135
|
end
|
|
130
|
-
|
|
131
|
-
true
|
|
132
136
|
end
|
|
133
137
|
|
|
134
138
|
# @return [String] the proper attribute name, with nesting considered.
|
|
135
139
|
def full_name(name, index: nil)
|
|
140
|
+
tracker = ParamScopeTracker.current
|
|
136
141
|
if nested?
|
|
137
142
|
# Find our containing element's name, and append ours.
|
|
138
|
-
|
|
143
|
+
resolved_index = index || tracker&.index_for(self)
|
|
144
|
+
"#{@parent.full_name(@element)}#{brackets(resolved_index)}#{brackets(name)}"
|
|
139
145
|
elsif lateral?
|
|
140
146
|
# Find the name of the element as if it was at the same nesting level
|
|
141
147
|
# as our parent. We need to forward our index upward to achieve this.
|
|
142
|
-
@parent.full_name(name, index:
|
|
148
|
+
@parent.full_name(name, index: tracker&.index_for(self))
|
|
143
149
|
else
|
|
144
150
|
# We must be the root scope, so no prefix needed.
|
|
145
151
|
name.to_s
|
|
@@ -174,28 +180,23 @@ module Grape
|
|
|
174
180
|
!@optional
|
|
175
181
|
end
|
|
176
182
|
|
|
177
|
-
def reset_index
|
|
178
|
-
@index = nil
|
|
179
|
-
end
|
|
180
|
-
|
|
181
183
|
protected
|
|
182
184
|
|
|
183
185
|
# Adds a parameter declaration to our list of validations.
|
|
184
186
|
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
|
|
185
|
-
def push_declared_params(attrs, opts
|
|
187
|
+
def push_declared_params(attrs, **opts)
|
|
186
188
|
opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope)
|
|
187
|
-
return @parent.push_declared_params(attrs, opts) if lateral?
|
|
189
|
+
return @parent.push_declared_params(attrs, **opts) if lateral?
|
|
188
190
|
|
|
189
191
|
push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as]
|
|
190
192
|
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
|
|
191
193
|
end
|
|
192
194
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def full_path
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def build_full_path
|
|
197
198
|
if nested?
|
|
198
|
-
|
|
199
|
+
@parent.full_path + [@element]
|
|
199
200
|
elsif lateral?
|
|
200
201
|
@parent.full_path
|
|
201
202
|
else
|
|
@@ -203,8 +204,6 @@ module Grape
|
|
|
203
204
|
end
|
|
204
205
|
end
|
|
205
206
|
|
|
206
|
-
private
|
|
207
|
-
|
|
208
207
|
# Add a new parameter which should be renamed when using the +#declared+
|
|
209
208
|
# method.
|
|
210
209
|
#
|
|
@@ -219,9 +218,9 @@ module Grape
|
|
|
219
218
|
api_route_setting[:renamed_params] = base
|
|
220
219
|
end
|
|
221
220
|
|
|
222
|
-
def require_required_and_optional_fields(context,
|
|
223
|
-
except_fields = Array.wrap(
|
|
224
|
-
using_fields =
|
|
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) }
|
|
225
224
|
|
|
226
225
|
if context == :all
|
|
227
226
|
optional_fields = except_fields
|
|
@@ -231,56 +230,55 @@ module Grape
|
|
|
231
230
|
optional_fields = using_fields
|
|
232
231
|
end
|
|
233
232
|
required_fields.each do |field|
|
|
234
|
-
field_opts =
|
|
233
|
+
field_opts = using[field]
|
|
235
234
|
raise ArgumentError, "required field not exist: #{field}" unless field_opts
|
|
236
235
|
|
|
237
236
|
requires(field, **field_opts)
|
|
238
237
|
end
|
|
239
238
|
optional_fields.each do |field|
|
|
240
|
-
field_opts =
|
|
239
|
+
field_opts = using[field]
|
|
241
240
|
optional(field, **field_opts) if field_opts
|
|
242
241
|
end
|
|
243
242
|
end
|
|
244
243
|
|
|
245
|
-
def require_optional_fields(context,
|
|
246
|
-
optional_fields =
|
|
244
|
+
def require_optional_fields(context, using:, except: nil)
|
|
245
|
+
optional_fields = using.keys
|
|
247
246
|
unless context == :all
|
|
248
|
-
except_fields = Array.wrap(
|
|
247
|
+
except_fields = Array.wrap(except)
|
|
249
248
|
optional_fields.delete_if { |f| except_fields.include?(f) }
|
|
250
249
|
end
|
|
251
250
|
optional_fields.each do |field|
|
|
252
|
-
field_opts =
|
|
251
|
+
field_opts = using[field]
|
|
253
252
|
optional(field, **field_opts) if field_opts
|
|
254
253
|
end
|
|
255
254
|
end
|
|
256
255
|
|
|
257
|
-
def validate_attributes(attrs, opts, &block)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
validates(attrs, validations)
|
|
256
|
+
def validate_attributes(attrs, **opts, &block)
|
|
257
|
+
opts[:type] ||= Array if block
|
|
258
|
+
validates(attrs, opts)
|
|
261
259
|
end
|
|
262
260
|
|
|
263
261
|
# Returns a new parameter scope, subordinate to the current one and nested
|
|
264
|
-
# under the
|
|
265
|
-
# @param
|
|
266
|
-
#
|
|
267
|
-
# @param
|
|
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
|
|
268
267
|
# is optional or not (and hence, whether this block's params will be).
|
|
269
268
|
# @yield parameter scope
|
|
270
|
-
def new_scope(
|
|
269
|
+
def new_scope(element, type:, as:, optional: false, &)
|
|
271
270
|
# if required params are grouped and no type or unsupported type is provided, raise an error
|
|
272
|
-
|
|
273
|
-
if attrs.first && !optional
|
|
271
|
+
if element && !optional
|
|
274
272
|
raise Grape::Exceptions::MissingGroupType if type.nil?
|
|
275
273
|
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
|
|
276
274
|
end
|
|
277
275
|
|
|
278
276
|
self.class.new(
|
|
279
277
|
api: @api,
|
|
280
|
-
element
|
|
281
|
-
element_renamed:
|
|
278
|
+
element:,
|
|
279
|
+
element_renamed: as,
|
|
282
280
|
parent: self,
|
|
283
|
-
optional
|
|
281
|
+
optional:,
|
|
284
282
|
type: type || Array,
|
|
285
283
|
group: @group,
|
|
286
284
|
&
|
|
@@ -289,46 +287,50 @@ module Grape
|
|
|
289
287
|
|
|
290
288
|
# Returns a new parameter scope, not nested under any current-level param
|
|
291
289
|
# but instead at the same level as the current scope.
|
|
292
|
-
# @param
|
|
293
|
-
#
|
|
294
|
-
# scope should only validate if this parameter from the above scope is
|
|
295
|
-
# 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
|
|
296
292
|
# @yield parameter scope
|
|
297
|
-
def new_lateral_scope(
|
|
293
|
+
def new_lateral_scope(dependent_on:, &)
|
|
298
294
|
self.class.new(
|
|
299
295
|
api: @api,
|
|
300
|
-
element: nil,
|
|
301
296
|
parent: self,
|
|
302
|
-
|
|
297
|
+
optional: @optional,
|
|
303
298
|
type: type == Array ? Array : Hash,
|
|
304
|
-
dependent_on
|
|
299
|
+
dependent_on:,
|
|
305
300
|
&
|
|
306
301
|
)
|
|
307
302
|
end
|
|
308
303
|
|
|
309
|
-
# Returns a new parameter scope, subordinate to the current one
|
|
310
|
-
#
|
|
311
|
-
# @param
|
|
312
|
-
# `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
|
|
313
307
|
# @yield parameter scope
|
|
314
|
-
def new_group_scope(
|
|
315
|
-
self.class.new(api: @api, parent: self, group
|
|
308
|
+
def new_group_scope(group, &)
|
|
309
|
+
self.class.new(api: @api, parent: self, group:, &)
|
|
316
310
|
end
|
|
317
311
|
|
|
318
|
-
# 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.
|
|
319
316
|
def configure_declared_params
|
|
320
317
|
push_renamed_param(full_path, @element_renamed) if @element_renamed
|
|
321
318
|
|
|
322
319
|
if nested?
|
|
323
|
-
@parent.push_declared_params [element => @declared_params]
|
|
320
|
+
@parent.push_declared_params [@element => @declared_params]
|
|
324
321
|
else
|
|
325
322
|
@api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
|
|
326
323
|
end
|
|
327
|
-
|
|
328
|
-
# params were stored in settings, it can be cleaned from the params scope
|
|
324
|
+
ensure
|
|
329
325
|
@declared_params = nil
|
|
330
326
|
end
|
|
331
327
|
|
|
328
|
+
def find_nearest_array_ancestor
|
|
329
|
+
scope = @parent
|
|
330
|
+
scope = scope.parent while scope && scope.type != Array
|
|
331
|
+
scope
|
|
332
|
+
end
|
|
333
|
+
|
|
332
334
|
def validates(attrs, validations)
|
|
333
335
|
coerce_type = infer_coercion(validations)
|
|
334
336
|
required = validations.key?(:presence)
|
|
@@ -349,7 +351,7 @@ module Grape
|
|
|
349
351
|
|
|
350
352
|
document_params attrs, validations, coerce_type, values, except_values
|
|
351
353
|
|
|
352
|
-
opts = derive_validator_options(validations)
|
|
354
|
+
opts = derive_validator_options(validations).freeze
|
|
353
355
|
|
|
354
356
|
# Validate for presence before any other validators
|
|
355
357
|
validates_presence(validations, attrs, opts)
|
|
@@ -357,7 +359,7 @@ module Grape
|
|
|
357
359
|
# Before we run the rest of the validators, let's handle
|
|
358
360
|
# whatever coercion so that we are working with correctly
|
|
359
361
|
# type casted values
|
|
360
|
-
coerce_type validations, attrs, required, opts
|
|
362
|
+
coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts
|
|
361
363
|
|
|
362
364
|
validations.each do |type, options|
|
|
363
365
|
# Don't try to look up validators for documentation params that don't have one.
|
|
@@ -430,7 +432,12 @@ module Grape
|
|
|
430
432
|
def coerce_type(validations, attrs, required, opts)
|
|
431
433
|
check_coerce_with(validations)
|
|
432
434
|
|
|
433
|
-
|
|
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]
|
|
434
441
|
|
|
435
442
|
coerce_options = {
|
|
436
443
|
type: validations[:coerce],
|
|
@@ -438,9 +445,6 @@ module Grape
|
|
|
438
445
|
message: validations[:coerce_message]
|
|
439
446
|
}
|
|
440
447
|
validate('coerce', coerce_options, attrs, required, opts)
|
|
441
|
-
validations.delete(:coerce_with)
|
|
442
|
-
validations.delete(:coerce)
|
|
443
|
-
validations.delete(:coerce_message)
|
|
444
448
|
end
|
|
445
449
|
|
|
446
450
|
def guess_coerce_type(coerce_type, *values_list)
|
|
@@ -464,15 +468,15 @@ module Grape
|
|
|
464
468
|
end
|
|
465
469
|
|
|
466
470
|
def validate(type, options, attrs, required, opts)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
@api.inheritable_setting.namespace_stackable[:validations] =
|
|
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
|
|
476
480
|
end
|
|
477
481
|
|
|
478
482
|
def validate_value_coercion(coerce_type, *values_list)
|
|
@@ -5,10 +5,10 @@ module Grape
|
|
|
5
5
|
class SingleAttributeIterator < AttributesIterator
|
|
6
6
|
private
|
|
7
7
|
|
|
8
|
-
def yield_attributes(val
|
|
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
|
|
@@ -4,11 +4,14 @@ module Grape
|
|
|
4
4
|
module Validations
|
|
5
5
|
module Validators
|
|
6
6
|
class AllOrNoneOfValidator < MultipleParamsBase
|
|
7
|
+
default_message_key :all_or_none
|
|
8
|
+
|
|
7
9
|
def validate_params!(params)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
known_keys = all_keys
|
|
11
|
+
keys = keys_in_common(params, known_keys)
|
|
12
|
+
return if keys.empty? || keys.length == @attrs.length
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
validation_error!(known_keys)
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -4,15 +4,20 @@ module Grape
|
|
|
4
4
|
module Validations
|
|
5
5
|
module Validators
|
|
6
6
|
class AllowBlankValidator < Base
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
default_message_key :blank
|
|
8
|
+
|
|
9
|
+
def initialize(attrs, options, required, scope, opts)
|
|
10
|
+
super
|
|
11
|
+
@value = option_value
|
|
12
|
+
end
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
def validate_param!(attr_name, params)
|
|
15
|
+
return if @value || !hash_like?(params)
|
|
12
16
|
|
|
17
|
+
value = scrub(params[attr_name])
|
|
13
18
|
return if value == false || value.present?
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
validation_error!(attr_name)
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -4,10 +4,13 @@ module Grape
|
|
|
4
4
|
module Validations
|
|
5
5
|
module Validators
|
|
6
6
|
class AtLeastOneOfValidator < MultipleParamsBase
|
|
7
|
+
default_message_key :at_least_one
|
|
8
|
+
|
|
7
9
|
def validate_params!(params)
|
|
8
|
-
|
|
10
|
+
known_keys = all_keys
|
|
11
|
+
return if hash_like?(params) && known_keys.intersect?(params.keys.map { |attr| @scope.full_name(attr) })
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
validation_error!(known_keys)
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
end
|
|
@@ -3,24 +3,64 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
5
|
module Validators
|
|
6
|
+
# Base class for all parameter validators.
|
|
7
|
+
#
|
|
8
|
+
# == Freeze contract
|
|
9
|
+
# Validator instances are shared across requests and are frozen after
|
|
10
|
+
# initialization (via +.new+). All inputs (+options+, +opts+, +attrs+)
|
|
11
|
+
# arrive pre-frozen from the DSL boundary, so subclass ivars derived
|
|
12
|
+
# from them are frozen by construction. Lazy ivar assignment
|
|
13
|
+
# (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
|
|
6
14
|
class Base
|
|
15
|
+
include Grape::Util::Translation
|
|
16
|
+
|
|
7
17
|
attr_reader :attrs
|
|
8
18
|
|
|
19
|
+
class << self
|
|
20
|
+
# Declares the default I18n message key used by +validation_error!+.
|
|
21
|
+
# Subclasses that only need a single fixed error message can declare it
|
|
22
|
+
# at the class level instead of overriding +initialize+:
|
|
23
|
+
#
|
|
24
|
+
# class MyValidator < Grape::Validations::Validators::Base
|
|
25
|
+
# default_message_key :my_error
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# The key is resolved through +message+, so a per-option +:message+
|
|
29
|
+
# override still takes precedence.
|
|
30
|
+
def default_message_key(key = nil)
|
|
31
|
+
if key
|
|
32
|
+
@default_message_key = key
|
|
33
|
+
else
|
|
34
|
+
@default_message_key || (superclass.respond_to?(:default_message_key) ? superclass.default_message_key : nil)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def new(...)
|
|
39
|
+
super.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inherited(klass)
|
|
43
|
+
super
|
|
44
|
+
Validations.register(klass)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
9
48
|
# Creates a new Validator from options specified
|
|
10
49
|
# by a +requires+ or +optional+ directive during
|
|
11
50
|
# parameter definition.
|
|
12
51
|
# @param attrs [Array] names of attributes to which the Validator applies
|
|
13
|
-
# @param options [Object] implementation-dependent Validator options
|
|
52
|
+
# @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
|
|
14
53
|
# @param required [Boolean] attribute(s) are required or optional
|
|
15
54
|
# @param scope [ParamsScope] parent scope for this Validator
|
|
16
55
|
# @param opts [Hash] additional validation options
|
|
17
56
|
def initialize(attrs, options, required, scope, opts)
|
|
18
|
-
@attrs = Array(attrs)
|
|
19
|
-
@
|
|
57
|
+
@attrs = Array(attrs).freeze
|
|
58
|
+
@options = Grape::Util::DeepFreeze.deep_freeze(options)
|
|
59
|
+
@option = @options # TODO: remove in next major release
|
|
20
60
|
@required = required
|
|
21
61
|
@scope = scope
|
|
22
|
-
@fail_fast = opts
|
|
23
|
-
@
|
|
62
|
+
@fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)
|
|
63
|
+
@exception_message = message(self.class.default_message_key) if self.class.default_message_key
|
|
24
64
|
end
|
|
25
65
|
|
|
26
66
|
# Validates a given request.
|
|
@@ -34,13 +74,18 @@ module Grape
|
|
|
34
74
|
validate!(request.params)
|
|
35
75
|
end
|
|
36
76
|
|
|
77
|
+
def fail_fast?
|
|
78
|
+
@fail_fast
|
|
79
|
+
end
|
|
80
|
+
|
|
37
81
|
# Validates a given parameter hash.
|
|
38
|
-
# @note Override #
|
|
82
|
+
# @note Override #validate_param! for per-parameter validation,
|
|
83
|
+
# or #validate if you need access to the entire request.
|
|
39
84
|
# @param params [Hash] parameters to validate
|
|
40
85
|
# @raise [Grape::Exceptions::Validation] if validation failed
|
|
41
86
|
# @return [void]
|
|
42
87
|
def validate!(params)
|
|
43
|
-
attributes = SingleAttributeIterator.new(
|
|
88
|
+
attributes = SingleAttributeIterator.new(@attrs, @scope, params)
|
|
44
89
|
# we collect errors inside array because
|
|
45
90
|
# there may be more than one error per field
|
|
46
91
|
array_errors = []
|
|
@@ -49,7 +94,7 @@ module Grape
|
|
|
49
94
|
next if !@scope.required? && empty_val
|
|
50
95
|
next unless @scope.meets_dependency?(val, params)
|
|
51
96
|
|
|
52
|
-
validate_param!(attr_name, val) if @required || (
|
|
97
|
+
validate_param!(attr_name, val) if @required || (hash_like?(val) && val.key?(attr_name))
|
|
53
98
|
rescue Grape::Exceptions::Validation => e
|
|
54
99
|
array_errors << e
|
|
55
100
|
end
|
|
@@ -57,23 +102,55 @@ module Grape
|
|
|
57
102
|
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
|
|
58
103
|
end
|
|
59
104
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
protected
|
|
106
|
+
|
|
107
|
+
# Validates a single attribute. Override in subclasses.
|
|
108
|
+
# @param attr_name [Symbol, String] the attribute name
|
|
109
|
+
# @param params [Hash] the parameter hash containing the attribute
|
|
110
|
+
# @raise [Grape::Exceptions::Validation] if validation failed
|
|
111
|
+
# @return [void]
|
|
112
|
+
def validate_param!(attr_name, params)
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def validation_error!(attr_name_or_params, message = @exception_message)
|
|
119
|
+
params = attr_name_or_params.is_a?(Array) ? attr_name_or_params : @scope.full_name(attr_name_or_params)
|
|
120
|
+
raise Grape::Exceptions::Validation.new(params:, message:)
|
|
63
121
|
end
|
|
64
122
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
options_key?(:message) ? options[:message] : default_key
|
|
123
|
+
def hash_like?(obj)
|
|
124
|
+
obj.respond_to?(:key?)
|
|
68
125
|
end
|
|
69
126
|
|
|
70
127
|
def options_key?(key, options = nil)
|
|
71
|
-
|
|
72
|
-
|
|
128
|
+
current_options = options || @options
|
|
129
|
+
hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
|
|
73
130
|
end
|
|
74
131
|
|
|
75
|
-
|
|
76
|
-
|
|
132
|
+
# Returns the effective message for a validation error.
|
|
133
|
+
# Prefers an explicit +:message+ option, then +default_key+.
|
|
134
|
+
# If both are nil, the block (if given) is called to compute a fallback —
|
|
135
|
+
# useful for validators that build a message Hash for deferred i18n interpolation.
|
|
136
|
+
# @example
|
|
137
|
+
# @exception_message = message(:presence) # symbol key or custom message
|
|
138
|
+
# @exception_message = message { build_hash_message } # computed fallback
|
|
139
|
+
def message(default_key = nil)
|
|
140
|
+
key = options_key?(:message) ? @options[:message] : default_key
|
|
141
|
+
return key unless key.nil?
|
|
142
|
+
|
|
143
|
+
yield if block_given?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def option_value
|
|
147
|
+
options_key?(:value) ? @options[:value] : @options
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def scrub(value)
|
|
151
|
+
return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding?
|
|
152
|
+
|
|
153
|
+
value.scrub
|
|
77
154
|
end
|
|
78
155
|
end
|
|
79
156
|
end
|