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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +109 -0
- data/CONTRIBUTING.md +2 -10
- data/README.md +137 -181
- data/UPGRADING.md +154 -0
- data/grape.gemspec +4 -4
- data/lib/grape/api/instance.rb +51 -104
- data/lib/grape/api.rb +29 -42
- data/lib/grape/content_types.rb +1 -4
- data/lib/grape/declared_params_handler.rb +118 -0
- data/lib/grape/dry_types.rb +48 -4
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +8 -67
- data/lib/grape/dsl/helpers.rb +59 -64
- data/lib/grape/dsl/inside_route.rb +28 -189
- data/lib/grape/dsl/logger.rb +3 -6
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +24 -51
- data/lib/grape/dsl/request_response.rb +136 -139
- data/lib/grape/dsl/routing.rb +240 -200
- data/lib/grape/dsl/settings.rb +23 -135
- data/lib/grape/dsl/validations.rb +38 -44
- data/lib/grape/endpoint.rb +169 -205
- data/lib/grape/error_formatter/base.rb +4 -2
- data/lib/grape/exceptions/base.rb +19 -45
- 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_group_type.rb +0 -2
- 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/unsupported_group_type.rb +0 -2
- 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/auth/dsl.rb +9 -10
- data/lib/grape/middleware/base.rb +6 -4
- data/lib/grape/middleware/error.rb +4 -14
- data/lib/grape/middleware/formatter.rb +11 -7
- data/lib/grape/middleware/stack.rb +3 -2
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
- data/lib/grape/middleware/versioner/base.rb +42 -40
- data/lib/grape/middleware/versioner/header.rb +2 -18
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +2 -2
- data/lib/grape/namespace.rb +15 -8
- data/lib/grape/params_builder/base.rb +2 -0
- data/lib/grape/params_builder.rb +2 -19
- data/lib/grape/request.rb +2 -10
- data/lib/grape/router/base_route.rb +14 -5
- data/lib/grape/router/greedy_route.rb +11 -5
- data/lib/grape/router/pattern.rb +6 -20
- data/lib/grape/router/route.rb +7 -11
- data/lib/grape/router.rb +42 -65
- data/lib/grape/util/api_description.rb +58 -0
- data/lib/grape/util/base_inheritable.rb +5 -2
- data/lib/grape/util/cache.rb +1 -0
- data/lib/grape/util/deep_freeze.rb +35 -0
- data/lib/grape/util/inheritable_setting.rb +8 -1
- data/lib/grape/util/media_type.rb +2 -2
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +35 -20
- data/lib/grape/validations/contract_scope.rb +2 -8
- 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_documentation.rb +50 -0
- data/lib/grape/validations/params_scope.rb +147 -156
- data/lib/grape/validations/single_attribute_iterator.rb +2 -2
- data/lib/grape/validations/types/array_coercer.rb +2 -3
- data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
- data/lib/grape/validations/types/primitive_coercer.rb +1 -28
- data/lib/grape/validations/types.rb +10 -25
- 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 -25
- data/lib/grape/validations/validators/coerce_validator.rb +15 -35
- data/lib/grape/validations/validators/contract_scope_validator.rb +9 -5
- 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/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- 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 +29 -23
- metadata +31 -26
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -17
- data/lib/grape/dsl/configuration.rb +0 -15
- 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/missing_option.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/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
- data/lib/grape/extensions/hash.rb +0 -27
- data/lib/grape/extensions/hashie/mash.rb +0 -24
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/validations/attributes_doc.rb +0 -60
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
48
|
-
# @
|
|
49
|
-
# this to be relevant,
|
|
50
|
-
# @
|
|
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
|
-
# @
|
|
53
|
-
# @
|
|
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
|
-
# @
|
|
57
|
-
# @
|
|
58
|
-
# @
|
|
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(
|
|
62
|
-
@element =
|
|
63
|
-
@element_renamed =
|
|
64
|
-
@parent =
|
|
65
|
-
@api =
|
|
66
|
-
@optional =
|
|
67
|
-
@type =
|
|
68
|
-
@group =
|
|
69
|
-
@dependent_on =
|
|
70
|
-
|
|
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
|
-
@
|
|
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)
|
|
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
|
-
|
|
101
|
-
|
|
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.
|
|
128
|
+
@dependent_on.all? do |dependency|
|
|
119
129
|
if dependency.is_a?(Hash)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def full_path
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def build_full_path
|
|
194
198
|
if nested?
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
api_route_setting[:renamed_params] = base
|
|
216
219
|
end
|
|
217
220
|
|
|
218
|
-
def require_required_and_optional_fields(context,
|
|
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 =
|
|
221
|
-
required_fields =
|
|
226
|
+
optional_fields = except_fields
|
|
227
|
+
required_fields = using_fields
|
|
222
228
|
else # context == :none
|
|
223
|
-
required_fields =
|
|
224
|
-
optional_fields =
|
|
229
|
+
required_fields = except_fields
|
|
230
|
+
optional_fields = using_fields
|
|
225
231
|
end
|
|
226
232
|
required_fields.each do |field|
|
|
227
|
-
field_opts =
|
|
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 =
|
|
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,
|
|
239
|
-
optional_fields =
|
|
244
|
+
def require_optional_fields(context, using:, except: nil)
|
|
245
|
+
optional_fields = using.keys
|
|
240
246
|
unless context == :all
|
|
241
|
-
except_fields = Array.wrap(
|
|
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 =
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
258
|
-
# @param
|
|
259
|
-
#
|
|
260
|
-
# @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
|
|
261
267
|
# is optional or not (and hence, whether this block's params will be).
|
|
262
268
|
# @yield parameter scope
|
|
263
|
-
def new_scope(
|
|
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
|
-
|
|
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
|
|
274
|
-
element_renamed:
|
|
278
|
+
element:,
|
|
279
|
+
element_renamed: as,
|
|
275
280
|
parent: self,
|
|
276
|
-
optional
|
|
281
|
+
optional:,
|
|
277
282
|
type: type || Array,
|
|
278
283
|
group: @group,
|
|
279
|
-
&
|
|
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
|
|
286
|
-
#
|
|
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(
|
|
293
|
+
def new_lateral_scope(dependent_on:, &)
|
|
291
294
|
self.class.new(
|
|
292
295
|
api: @api,
|
|
293
|
-
element: nil,
|
|
294
296
|
parent: self,
|
|
295
|
-
|
|
297
|
+
optional: @optional,
|
|
296
298
|
type: type == Array ? Array : Hash,
|
|
297
|
-
dependent_on
|
|
298
|
-
&
|
|
299
|
+
dependent_on:,
|
|
300
|
+
&
|
|
299
301
|
)
|
|
300
302
|
end
|
|
301
303
|
|
|
302
|
-
# Returns a new parameter scope, subordinate to the current one
|
|
303
|
-
#
|
|
304
|
-
# @param
|
|
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(
|
|
308
|
-
self.class.new(api: @api, parent: self, group
|
|
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
|
|
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
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
350
|
+
validate_value_coercion(coerce_type, values, except_values)
|
|
358
351
|
|
|
359
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
432
|
+
def coerce_type(validations, attrs, required, opts)
|
|
440
433
|
check_coerce_with(validations)
|
|
441
434
|
|
|
442
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
@api.namespace_stackable
|
|
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,
|
|
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,
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
@
|
|
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
|
|
49
|
+
rescue Dry::Types::CoercionError
|
|
57
50
|
InvalidValue.new
|
|
58
51
|
end
|
|
59
52
|
|
|
60
53
|
protected
|
|
61
54
|
|
|
62
|
-
attr_reader :
|
|
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
|
-
@
|
|
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)
|