grape 2.4.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +351 -0
- data/CONTRIBUTING.md +18 -10
- data/README.md +457 -228
- data/UPGRADING.md +1240 -1
- data/grape.gemspec +7 -7
- data/lib/grape/api/instance.rb +65 -116
- data/lib/grape/api.rb +53 -62
- data/lib/grape/content_types.rb +58 -3
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +116 -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 +24 -67
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/helpers.rb +80 -65
- data/lib/grape/dsl/inside_route.rb +56 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +86 -93
- data/lib/grape/dsl/request_response.rb +139 -139
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +328 -203
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +38 -44
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +285 -245
- data/lib/grape/env.rb +8 -3
- data/lib/grape/error_formatter/base.rb +27 -21
- data/lib/grape/error_formatter/json.rb +6 -0
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/base.rb +20 -44
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- 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_error_formatter.rb +11 -0
- 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 +25 -7
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +21 -23
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +6 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +3 -5
- data/lib/grape/middleware/auth/base.rb +4 -3
- data/lib/grape/middleware/auth/dsl.rb +9 -23
- data/lib/grape/middleware/base.rb +41 -36
- data/lib/grape/middleware/error.rb +271 -73
- data/lib/grape/middleware/formatter.rb +152 -63
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +20 -11
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
- data/lib/grape/middleware/versioner/base.rb +40 -48
- data/lib/grape/middleware/versioner/header.rb +5 -23
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +54 -7
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +19 -11
- data/lib/grape/params_builder/base.rb +2 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/params_builder.rb +2 -19
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +8 -72
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +57 -20
- data/lib/grape/router/base_route.rb +53 -7
- data/lib/grape/router/greedy_route.rb +16 -6
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +65 -37
- data/lib/grape/router/route.rb +66 -18
- data/lib/grape/router.rb +126 -115
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -2
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +13 -9
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +13 -3
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +39 -10
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -29
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +8 -13
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +217 -304
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -10
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +14 -33
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -51
- data/lib/grape/validations/validations_spec.rb +164 -0
- 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 +118 -34
- data/lib/grape/validations/validators/coerce_validator.rb +25 -37
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
- data/lib/grape/validations/validators/default_validator.rb +13 -20
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +14 -4
- data/lib/grape/validations/validators/length_validator.rb +23 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- 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 +7 -15
- data/lib/grape/validations/validators/values_validator.rb +34 -26
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +58 -31
- metadata +62 -39
- 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/middleware/globals.rb +0 -14
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -29
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
- 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, :array_depth, :full_path
|
|
7
|
+
|
|
8
|
+
def qualifying_params
|
|
9
|
+
ParamScopeTracker.current&.qualifying_params(self)
|
|
10
|
+
end
|
|
8
11
|
|
|
9
12
|
include Grape::DSL::Parameters
|
|
13
|
+
include Grape::Validations::ParamsDocumentation
|
|
10
14
|
|
|
11
15
|
# There are a number of documentation options on entities that don't have
|
|
12
16
|
# corresponding validators. Since there is nowhere that enumerates them all,
|
|
13
17
|
# we maintain a list of them here and skip looking up validators for them.
|
|
14
18
|
RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
|
|
15
19
|
|
|
20
|
+
SPECIAL_JSON = [JSON, Array[JSON]].freeze
|
|
21
|
+
|
|
16
22
|
class Attr
|
|
17
|
-
|
|
23
|
+
attr_reader :key, :scope
|
|
18
24
|
|
|
19
25
|
# Open up a new ParamsScope::Attr
|
|
20
26
|
# @param key [Hash, Symbol] key of attr
|
|
@@ -32,9 +38,10 @@ module Grape
|
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def self.attr_key(declared_param_attr)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
case declared_param_attr
|
|
42
|
+
when self
|
|
43
|
+
attr_key(declared_param_attr.key)
|
|
44
|
+
when Hash
|
|
38
45
|
declared_param_attr.transform_values { |value| attrs_keys(value) }
|
|
39
46
|
else
|
|
40
47
|
declared_param_attr
|
|
@@ -44,40 +51,47 @@ module Grape
|
|
|
44
51
|
|
|
45
52
|
# Open up a new ParamsScope, allowing parameter definitions per
|
|
46
53
|
# Grape::DSL::Params.
|
|
47
|
-
# @param
|
|
48
|
-
# @
|
|
49
|
-
# this to be relevant,
|
|
50
|
-
# @
|
|
54
|
+
# @param api [API] the API endpoint to modify
|
|
55
|
+
# @param element [Symbol] the element that contains this scope; for
|
|
56
|
+
# this to be relevant, parent must be set
|
|
57
|
+
# @param element_renamed [Symbol, nil] whenever this scope should
|
|
51
58
|
# be renamed and to what, given +nil+ no renaming is done
|
|
52
|
-
# @
|
|
53
|
-
# @
|
|
54
|
-
# @option opts :optional [Boolean] whether or not this scope needs to have
|
|
59
|
+
# @param parent [ParamsScope] the scope containing this scope
|
|
60
|
+
# @param optional [Boolean] whether or not this scope needs to have
|
|
55
61
|
# any parameters set or not
|
|
56
|
-
# @
|
|
57
|
-
# @
|
|
58
|
-
# @
|
|
62
|
+
# @param type [Class] a type meant to govern this scope (deprecated)
|
|
63
|
+
# @param type [Hash] group options for this scope
|
|
64
|
+
# @param dependent_on [Symbol] if present, this scope should only
|
|
59
65
|
# validate if this param is present in the parent scope
|
|
60
66
|
# @yield the instance context, open for parameter definitions
|
|
61
|
-
def initialize(
|
|
62
|
-
@element =
|
|
63
|
-
@element_renamed =
|
|
64
|
-
@parent =
|
|
65
|
-
@api =
|
|
66
|
-
@optional =
|
|
67
|
-
@type =
|
|
68
|
-
@group =
|
|
69
|
-
@dependent_on =
|
|
70
|
-
|
|
67
|
+
def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block)
|
|
68
|
+
@element = element
|
|
69
|
+
@element_renamed = element_renamed
|
|
70
|
+
@parent = parent
|
|
71
|
+
@api = api
|
|
72
|
+
@optional = optional
|
|
73
|
+
@type = type
|
|
74
|
+
@group = group
|
|
75
|
+
@dependent_on = dependent_on
|
|
76
|
+
# Must be an ivar: push_declared_params is dispatched on self during
|
|
77
|
+
# instance_eval, so local variables from initialize are unreachable.
|
|
78
|
+
# configure_declared_params consumes it and clears @declared_params to nil.
|
|
71
79
|
@declared_params = []
|
|
72
|
-
@
|
|
80
|
+
@full_path = build_full_path
|
|
81
|
+
# Read by the validators instantiated from the block below, so it has to
|
|
82
|
+
# be settled before the instance_eval.
|
|
83
|
+
@array_depth = find_array_depth
|
|
73
84
|
|
|
74
85
|
instance_eval(&block) if block
|
|
75
86
|
|
|
76
87
|
configure_declared_params
|
|
88
|
+
@nearest_array_ancestor = find_nearest_array_ancestor
|
|
89
|
+
freeze
|
|
77
90
|
end
|
|
78
91
|
|
|
79
92
|
def configuration
|
|
80
|
-
|
|
93
|
+
config = @api.configuration
|
|
94
|
+
config.is_a?(Grape::Util::Lazy::Base) ? config.evaluate : config
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
# @return [Boolean] whether or not this entire scope needs to be
|
|
@@ -87,9 +101,9 @@ module Grape
|
|
|
87
101
|
|
|
88
102
|
return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
|
|
89
103
|
return false unless meets_dependency?(scoped_params, parameters)
|
|
90
|
-
return true if parent.nil?
|
|
104
|
+
return true if @parent.nil?
|
|
91
105
|
|
|
92
|
-
parent.should_validate?(parameters)
|
|
106
|
+
@parent.should_validate?(parameters)
|
|
93
107
|
end
|
|
94
108
|
|
|
95
109
|
def meets_dependency?(params, request_params)
|
|
@@ -97,8 +111,9 @@ module Grape
|
|
|
97
111
|
return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
|
|
98
112
|
|
|
99
113
|
if params.is_a?(Array)
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
|
|
115
|
+
ParamScopeTracker.current&.store_qualifying_params(self, filtered)
|
|
116
|
+
return filtered.present?
|
|
102
117
|
end
|
|
103
118
|
|
|
104
119
|
meets_hash_dependency?(params)
|
|
@@ -115,28 +130,27 @@ module Grape
|
|
|
115
130
|
# params might be anything what looks like a hash, so it must implement a `key?` method
|
|
116
131
|
return false unless params.respond_to?(:key?)
|
|
117
132
|
|
|
118
|
-
@dependent_on.
|
|
133
|
+
@dependent_on.all? do |dependency|
|
|
119
134
|
if dependency.is_a?(Hash)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return false
|
|
135
|
+
key, callable = dependency.first
|
|
136
|
+
callable.call(params[key])
|
|
137
|
+
else
|
|
138
|
+
params[dependency].present?
|
|
125
139
|
end
|
|
126
140
|
end
|
|
127
|
-
|
|
128
|
-
true
|
|
129
141
|
end
|
|
130
142
|
|
|
131
143
|
# @return [String] the proper attribute name, with nesting considered.
|
|
132
144
|
def full_name(name, index: nil)
|
|
145
|
+
tracker = ParamScopeTracker.current
|
|
133
146
|
if nested?
|
|
134
147
|
# Find our containing element's name, and append ours.
|
|
135
|
-
|
|
148
|
+
resolved_index = index || tracker&.index_for(self)
|
|
149
|
+
"#{@parent.full_name(@element)}#{brackets(resolved_index)}#{brackets(name)}"
|
|
136
150
|
elsif lateral?
|
|
137
151
|
# Find the name of the element as if it was at the same nesting level
|
|
138
152
|
# as our parent. We need to forward our index upward to achieve this.
|
|
139
|
-
@parent.full_name(name, index:
|
|
153
|
+
@parent.full_name(name, index: tracker&.index_for(self))
|
|
140
154
|
else
|
|
141
155
|
# We must be the root scope, so no prefix needed.
|
|
142
156
|
name.to_s
|
|
@@ -158,6 +172,17 @@ module Grape
|
|
|
158
172
|
@parent && @element
|
|
159
173
|
end
|
|
160
174
|
|
|
175
|
+
# Whether this scope's params resolve to one entry per element, which is
|
|
176
|
+
# what makes both an element index and a nesting level meaningful.
|
|
177
|
+
#
|
|
178
|
+
# +type: Array[JSON]+ counts as much as +type: Array+ does. It is easy to
|
|
179
|
+
# miss because it evaluates to the Array *instance* +[JSON]+ rather than
|
|
180
|
+
# the Array class, so an +== Array+ test quietly excluded it.
|
|
181
|
+
# @return [Boolean]
|
|
182
|
+
def iterates_elements?
|
|
183
|
+
@type == Array || @type == SPECIAL_JSON.last
|
|
184
|
+
end
|
|
185
|
+
|
|
161
186
|
# A lateral scope is subordinate to its parent, but its keys are at the
|
|
162
187
|
# same level as its parent and thus is not contained within an element.
|
|
163
188
|
# @return [Boolean] whether or not this scope is lateral
|
|
@@ -171,37 +196,31 @@ module Grape
|
|
|
171
196
|
!@optional
|
|
172
197
|
end
|
|
173
198
|
|
|
174
|
-
def reset_index
|
|
175
|
-
@index = nil
|
|
176
|
-
end
|
|
177
|
-
|
|
178
199
|
protected
|
|
179
200
|
|
|
180
201
|
# Adds a parameter declaration to our list of validations.
|
|
181
202
|
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
# +declared_params_scope+ is the scope an attribute is recorded against.
|
|
204
|
+
# It defaults to the receiver and is forwarded unchanged when a lateral
|
|
205
|
+
# scope hands the push to its parent, so an attribute declared inside a
|
|
206
|
+
# +given+ block stays attributed to the lateral scope that declared it
|
|
207
|
+
# rather than to the parent that stores it.
|
|
208
|
+
def push_declared_params(attrs, as: nil, declared_params_scope: self)
|
|
209
|
+
return @parent.push_declared_params(attrs, as:, declared_params_scope:) if lateral?
|
|
185
210
|
|
|
186
|
-
push_renamed_param(full_path + [attrs.first],
|
|
187
|
-
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr,
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Get the full path of the parameter scope in the hierarchy.
|
|
191
|
-
#
|
|
192
|
-
# @return [Array<Symbol>] the nesting/path of the current parameter scope
|
|
193
|
-
def full_path
|
|
194
|
-
if nested?
|
|
195
|
-
(@parent.full_path + [@element])
|
|
196
|
-
elsif lateral?
|
|
197
|
-
@parent.full_path
|
|
198
|
-
else
|
|
199
|
-
[]
|
|
200
|
-
end
|
|
211
|
+
push_renamed_param(full_path + [attrs.first], as) if as
|
|
212
|
+
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, declared_params_scope) })
|
|
201
213
|
end
|
|
202
214
|
|
|
203
215
|
private
|
|
204
216
|
|
|
217
|
+
def build_full_path
|
|
218
|
+
return @parent.full_path + [@element] if nested?
|
|
219
|
+
return @parent.full_path if lateral?
|
|
220
|
+
|
|
221
|
+
[]
|
|
222
|
+
end
|
|
223
|
+
|
|
205
224
|
# Add a new parameter which should be renamed when using the +#declared+
|
|
206
225
|
# method.
|
|
207
226
|
#
|
|
@@ -210,329 +229,223 @@ module Grape
|
|
|
210
229
|
# @param new_name [String, Symbol] the new name of the parameter (the
|
|
211
230
|
# renamed name, with the +as: ...+ semantic)
|
|
212
231
|
def push_renamed_param(path, new_name)
|
|
213
|
-
|
|
214
|
-
base[Array(path).map(&:to_s)] = new_name.to_s
|
|
215
|
-
@api.route_setting(:renamed_params, base)
|
|
232
|
+
@api.inheritable_setting.add_route_renamed_param(Array(path).map(&:to_s), new_name.to_s)
|
|
216
233
|
end
|
|
217
234
|
|
|
218
|
-
def require_required_and_optional_fields(context,
|
|
235
|
+
def require_required_and_optional_fields(context, using:, except: nil)
|
|
236
|
+
except_fields = Array.wrap(except)
|
|
237
|
+
using_fields = using.keys.delete_if { |f| except_fields.include?(f) }
|
|
238
|
+
|
|
219
239
|
if context == :all
|
|
220
|
-
optional_fields =
|
|
221
|
-
required_fields =
|
|
240
|
+
optional_fields = except_fields
|
|
241
|
+
required_fields = using_fields
|
|
222
242
|
else # context == :none
|
|
223
|
-
required_fields =
|
|
224
|
-
optional_fields =
|
|
243
|
+
required_fields = except_fields
|
|
244
|
+
optional_fields = using_fields
|
|
225
245
|
end
|
|
226
246
|
required_fields.each do |field|
|
|
227
|
-
field_opts =
|
|
228
|
-
raise ArgumentError, "required field not exist:
|
|
247
|
+
field_opts = using[field]
|
|
248
|
+
raise ArgumentError, "required field not exist: `#{field}`" unless field_opts
|
|
229
249
|
|
|
230
|
-
requires(field, field_opts)
|
|
250
|
+
requires(field, **field_opts)
|
|
231
251
|
end
|
|
232
252
|
optional_fields.each do |field|
|
|
233
|
-
field_opts =
|
|
234
|
-
optional(field, field_opts) if field_opts
|
|
253
|
+
field_opts = using[field]
|
|
254
|
+
optional(field, **field_opts) if field_opts
|
|
235
255
|
end
|
|
236
256
|
end
|
|
237
257
|
|
|
238
|
-
def require_optional_fields(context,
|
|
239
|
-
optional_fields =
|
|
258
|
+
def require_optional_fields(context, using:, except: nil)
|
|
259
|
+
optional_fields = using.keys
|
|
240
260
|
unless context == :all
|
|
241
|
-
except_fields = Array.wrap(
|
|
261
|
+
except_fields = Array.wrap(except)
|
|
242
262
|
optional_fields.delete_if { |f| except_fields.include?(f) }
|
|
243
263
|
end
|
|
244
264
|
optional_fields.each do |field|
|
|
245
|
-
field_opts =
|
|
246
|
-
optional(field, field_opts) if field_opts
|
|
265
|
+
field_opts = using[field]
|
|
266
|
+
optional(field, **field_opts) if field_opts
|
|
247
267
|
end
|
|
248
268
|
end
|
|
249
269
|
|
|
250
|
-
def validate_attributes(attrs, opts, &block)
|
|
251
|
-
validations = opts.clone
|
|
252
|
-
validations[:type] ||= Array if block
|
|
253
|
-
validates(attrs, validations)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
270
|
# Returns a new parameter scope, subordinate to the current one and nested
|
|
257
|
-
# under the
|
|
258
|
-
# @param
|
|
259
|
-
#
|
|
260
|
-
# @param
|
|
271
|
+
# under the given element.
|
|
272
|
+
# @param element [Symbol] the parameter name under which this scope is nested
|
|
273
|
+
# @param type [Class] the type governing this scope
|
|
274
|
+
# @param as [Symbol, nil] optional renamed name for the element
|
|
275
|
+
# @param optional [Boolean] whether the parameter this scope is nested under
|
|
261
276
|
# is optional or not (and hence, whether this block's params will be).
|
|
262
277
|
# @yield parameter scope
|
|
263
|
-
def new_scope(
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
278
|
+
def new_scope(element, type:, as:, optional: false, &)
|
|
279
|
+
# A group needs a type: it says whether the nested params sit under one
|
|
280
|
+
# object or repeat in a list, and without it the `type || Array` below
|
|
281
|
+
# would quietly pick one. Checked here for `requires` and `optional`
|
|
282
|
+
# alike — `new_scope` is only reached from their block branch.
|
|
283
|
+
if element
|
|
267
284
|
raise Grape::Exceptions::MissingGroupType if type.nil?
|
|
268
285
|
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
|
|
269
286
|
end
|
|
270
287
|
|
|
271
288
|
self.class.new(
|
|
272
289
|
api: @api,
|
|
273
|
-
element
|
|
274
|
-
element_renamed:
|
|
290
|
+
element:,
|
|
291
|
+
element_renamed: as,
|
|
275
292
|
parent: self,
|
|
276
|
-
optional
|
|
293
|
+
optional:,
|
|
277
294
|
type: type || Array,
|
|
278
295
|
group: @group,
|
|
279
|
-
&
|
|
296
|
+
&
|
|
280
297
|
)
|
|
281
298
|
end
|
|
282
299
|
|
|
283
300
|
# Returns a new parameter scope, not nested under any current-level param
|
|
284
301
|
# but instead at the same level as the current scope.
|
|
285
|
-
# @param
|
|
286
|
-
#
|
|
287
|
-
# scope should only validate if this parameter from the above scope is
|
|
288
|
-
# present
|
|
302
|
+
# @param dependent_on [Symbol] if given, specifies that this scope should
|
|
303
|
+
# only validate if this parameter from the above scope is present
|
|
289
304
|
# @yield parameter scope
|
|
290
|
-
def new_lateral_scope(
|
|
305
|
+
def new_lateral_scope(dependent_on:, &)
|
|
291
306
|
self.class.new(
|
|
292
307
|
api: @api,
|
|
293
|
-
element: nil,
|
|
294
308
|
parent: self,
|
|
295
|
-
|
|
309
|
+
optional: @optional,
|
|
296
310
|
type: type == Array ? Array : Hash,
|
|
297
|
-
dependent_on
|
|
298
|
-
&
|
|
311
|
+
dependent_on:,
|
|
312
|
+
&
|
|
299
313
|
)
|
|
300
314
|
end
|
|
301
315
|
|
|
302
|
-
# Returns a new parameter scope, subordinate to the current one
|
|
303
|
-
#
|
|
304
|
-
# @param
|
|
305
|
-
# `optional` invocation that opened this scope.
|
|
316
|
+
# Returns a new parameter scope, subordinate to the current one, sharing
|
|
317
|
+
# the given group options with all parameters defined within.
|
|
318
|
+
# @param group [Hash] common options to merge into each parameter in the scope
|
|
306
319
|
# @yield parameter scope
|
|
307
|
-
def new_group_scope(
|
|
308
|
-
self.class.new(api: @api, parent: self, group
|
|
320
|
+
def new_group_scope(group, &)
|
|
321
|
+
self.class.new(api: @api, parent: self, group:, &)
|
|
309
322
|
end
|
|
310
323
|
|
|
311
|
-
# Pushes declared params to parent or settings
|
|
324
|
+
# Pushes declared params to parent or settings, then clears @declared_params.
|
|
325
|
+
# Clearing here (rather than in initialize) keeps the lifecycle ownership in
|
|
326
|
+
# one place: this method both consumes and invalidates the ivar so that
|
|
327
|
+
# push_declared_params cannot be called on the frozen scope later.
|
|
312
328
|
def configure_declared_params
|
|
313
329
|
push_renamed_param(full_path, @element_renamed) if @element_renamed
|
|
330
|
+
return @parent.push_declared_params [{ @element => @declared_params }] if nested?
|
|
314
331
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
else
|
|
318
|
-
@api.namespace_stackable(:declared_params, @declared_params)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
# params were stored in settings, it can be cleaned from the params scope
|
|
332
|
+
@api.inheritable_setting.add_declared_params(@declared_params)
|
|
333
|
+
ensure
|
|
322
334
|
@declared_params = nil
|
|
323
335
|
end
|
|
324
336
|
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
doc.type = coerce_type
|
|
337
|
+
def find_nearest_array_ancestor
|
|
338
|
+
scope = @parent
|
|
339
|
+
scope = scope.parent while scope && !scope.iterates_elements?
|
|
340
|
+
scope
|
|
341
|
+
end
|
|
332
342
|
|
|
333
|
-
|
|
343
|
+
# Every element-iterating scope on the chain adds one level of nesting to
|
|
344
|
+
# what {#params} returns, because +map_params+ maps over the array it
|
|
345
|
+
# resolved from the parent. Counting them tells {AttributesIterator} how
|
|
346
|
+
# deep the declaration says the params for this scope may legitimately be.
|
|
347
|
+
def find_array_depth
|
|
348
|
+
(iterates_elements? ? 1 : 0) + (@parent&.array_depth || 0)
|
|
349
|
+
end
|
|
334
350
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
# +required+ is the DSL's own signal — +requires+ passes true, +optional+
|
|
352
|
+
# false. It used to travel as a +:presence+ key that +requires+ wrote into
|
|
353
|
+
# the caller's option Hash, which is why a user-supplied +presence:+ was
|
|
354
|
+
# silently overwritten there and silently honoured by +optional+, where
|
|
355
|
+
# nothing overwrote it. The key is built here from the flag instead, after
|
|
356
|
+
# the caller has merged in any enclosing +with+ attributes, so a
|
|
357
|
+
# group-level +message:+ reaches the presence validator.
|
|
358
|
+
#
|
|
359
|
+
# A +presence:+ supplied by an API still decides the outcome exactly as it
|
|
360
|
+
# used to — deprecated rather than dropped, so nothing changes under an
|
|
361
|
+
# API that relies on it until the key is ignored outright.
|
|
362
|
+
def validates(attrs, validations, required: false)
|
|
363
|
+
if validations.key?(:presence)
|
|
364
|
+
Grape.deprecator.warn('Passing a `presence` option is deprecated and it will be ignored in a future release. Declare the parameter with `requires` to make it required, `optional` to make it optional.')
|
|
341
365
|
end
|
|
342
366
|
|
|
343
|
-
|
|
367
|
+
declared = required ? validations.merge(presence: { value: true, message: validations[:message] }) : validations
|
|
368
|
+
declared = declared.merge(oneof: collected_oneof(declared)) if declared.key?(:oneof)
|
|
369
|
+
spec = ValidationsSpec.from(declared)
|
|
344
370
|
|
|
345
|
-
|
|
371
|
+
document_params(attrs, spec)
|
|
346
372
|
|
|
347
|
-
#
|
|
348
|
-
#
|
|
373
|
+
# Presence runs first — `required` is forwarded to every subsequent
|
|
374
|
+
# validator (some short-circuit on it).
|
|
375
|
+
validate_presence(spec, attrs)
|
|
349
376
|
|
|
350
|
-
#
|
|
351
|
-
|
|
377
|
+
# Coerce runs second — later validators see the typed value.
|
|
378
|
+
validate_coerce(spec, attrs)
|
|
352
379
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
# type should be compatible with values array, if both exist
|
|
357
|
-
validate_value_coercion(coerce_type, values, except_values, excepts)
|
|
358
|
-
|
|
359
|
-
doc.document attrs
|
|
360
|
-
|
|
361
|
-
opts = derive_validator_options(validations)
|
|
362
|
-
|
|
363
|
-
# Validate for presence before any other validators
|
|
364
|
-
validates_presence(validations, attrs, doc, opts)
|
|
365
|
-
|
|
366
|
-
# Before we run the rest of the validators, let's handle
|
|
367
|
-
# whatever coercion so that we are working with correctly
|
|
368
|
-
# type casted values
|
|
369
|
-
coerce_type validations, attrs, doc, opts
|
|
370
|
-
|
|
371
|
-
validations.each do |type, options|
|
|
372
|
-
# Don't try to look up validators for documentation params that don't have one.
|
|
373
|
-
next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
|
|
374
|
-
|
|
375
|
-
validate(type, options, attrs, doc, opts)
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
# Validate and comprehend the +:type+, +:types+, and +:coerce_with+
|
|
380
|
-
# options that have been supplied to the parameter declaration.
|
|
381
|
-
# The +:type+ and +:types+ options will be removed from the
|
|
382
|
-
# validations list, replaced appropriately with +:coerce+ and
|
|
383
|
-
# +:coerce_with+ options that will later be passed to
|
|
384
|
-
# {Validators::CoerceValidator}. The type that is returned may be
|
|
385
|
-
# used for documentation and further validation of parameter
|
|
386
|
-
# options.
|
|
387
|
-
#
|
|
388
|
-
# @param validations [Hash] list of validations supplied to the
|
|
389
|
-
# parameter declaration
|
|
390
|
-
# @return [class-like] type to which the parameter will be coerced
|
|
391
|
-
# @raise [ArgumentError] if the given type options are invalid
|
|
392
|
-
def infer_coercion(validations)
|
|
393
|
-
raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
|
|
394
|
-
|
|
395
|
-
validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
|
|
396
|
-
validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
|
|
397
|
-
validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
|
|
398
|
-
validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
|
|
399
|
-
|
|
400
|
-
validations.delete(:types) if validations.key?(:types)
|
|
401
|
-
|
|
402
|
-
coerce_type = validations[:coerce]
|
|
403
|
-
|
|
404
|
-
# Special case - when the argument is a single type that is a
|
|
405
|
-
# variant-type collection.
|
|
406
|
-
if Types.multiple?(coerce_type) && validations.key?(:type)
|
|
407
|
-
validations[:coerce] = Types::VariantCollectionCoercer.new(
|
|
408
|
-
coerce_type,
|
|
409
|
-
validations.delete(:coerce_with)
|
|
410
|
-
)
|
|
380
|
+
spec.validator_entries.each do |type, options|
|
|
381
|
+
validate(type, options, attrs, spec.required?, spec.shared_opts)
|
|
411
382
|
end
|
|
412
|
-
validations.delete(:type)
|
|
413
|
-
|
|
414
|
-
coerce_type
|
|
415
383
|
end
|
|
416
384
|
|
|
417
|
-
# Enforce correct usage of :coerce_with
|
|
418
|
-
# We do not allow coercion without a type, nor with
|
|
419
|
-
#
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
|
|
425
|
-
|
|
426
|
-
# but not special JSON types, which
|
|
427
|
-
# already imply coercion method
|
|
428
|
-
return if [JSON, Array[JSON]].exclude? validations[:coerce]
|
|
385
|
+
# Enforce correct usage of :coerce_with on a CoerceOptions.
|
|
386
|
+
# We do not allow coercion without a type, nor with +JSON+ as a type
|
|
387
|
+
# since that defines its own coercion method.
|
|
388
|
+
def check_coerce_with(coerce_options)
|
|
389
|
+
return unless coerce_options.coerce_method
|
|
390
|
+
raise ArgumentError, 'must supply type for coerce_with' unless coerce_options.type
|
|
391
|
+
return unless SPECIAL_JSON.include?(coerce_options.type)
|
|
429
392
|
|
|
430
393
|
raise ArgumentError, 'coerce_with disallowed for type: JSON'
|
|
431
394
|
end
|
|
432
395
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
# This validation has special handling since it is
|
|
436
|
-
# composited from more than one +requires+/+optional+
|
|
437
|
-
# parameter, and needs to be run before most other
|
|
438
|
-
# validations.
|
|
439
|
-
def coerce_type(validations, attrs, doc, opts)
|
|
440
|
-
check_coerce_with(validations)
|
|
396
|
+
def validate_presence(spec, attrs)
|
|
397
|
+
return unless spec.required?
|
|
441
398
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
coerce_options = {
|
|
445
|
-
type: validations[:coerce],
|
|
446
|
-
method: validations[:coerce_with],
|
|
447
|
-
message: validations[:coerce_message]
|
|
448
|
-
}
|
|
449
|
-
validate('coerce', coerce_options, attrs, doc, opts)
|
|
450
|
-
validations.delete(:coerce_with)
|
|
451
|
-
validations.delete(:coerce)
|
|
452
|
-
validations.delete(:coerce_message)
|
|
399
|
+
validate('presence', spec.presence_options, attrs, true, spec.shared_opts)
|
|
453
400
|
end
|
|
454
401
|
|
|
455
|
-
def
|
|
456
|
-
|
|
402
|
+
def validate_coerce(spec, attrs)
|
|
403
|
+
coerce_options = spec.coerce_options
|
|
404
|
+
check_coerce_with(coerce_options)
|
|
405
|
+
# Falsy check is intentional: when a remountable API is first evaluated
|
|
406
|
+
# on its base instance (no configuration supplied yet),
|
|
407
|
+
# configuration[:some_type] evaluates to nil. Skipping instantiation
|
|
408
|
+
# here is correct — the real mounted instance will replay this step
|
|
409
|
+
# with the actual type value.
|
|
410
|
+
return unless coerce_options.type
|
|
457
411
|
|
|
458
|
-
|
|
459
|
-
next if !values || values.is_a?(Proc)
|
|
460
|
-
return values.first.class if values.is_a?(Range) || !values.empty?
|
|
461
|
-
end
|
|
462
|
-
coerce_type
|
|
412
|
+
validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
|
|
463
413
|
end
|
|
464
414
|
|
|
465
|
-
|
|
466
|
-
|
|
415
|
+
# Translate a `oneof: [proc, proc, ...]` declaration into a list of
|
|
416
|
+
# captured validator arrays — one array per variant. Each variant's
|
|
417
|
+
# block is evaluated in its own +ParamsScope+ backed by an
|
|
418
|
+
# {OneofCollector} so the full params DSL is available inside variants
|
|
419
|
+
# and the resulting validators are kept out of the real API's
|
|
420
|
+
# registration list.
|
|
421
|
+
# Returns the collected variants rather than writing them back into
|
|
422
|
+
# +validations+, which is the options Hash the +requires+/+optional+ call
|
|
423
|
+
# site built.
|
|
424
|
+
def collected_oneof(validations)
|
|
425
|
+
raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
|
|
467
426
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
|
|
472
|
-
end
|
|
427
|
+
variants = validations[:oneof]
|
|
428
|
+
raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
|
|
429
|
+
raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
|
|
473
430
|
|
|
474
|
-
|
|
475
|
-
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
|
|
476
|
-
unless Array(default).none? { |def_val| excepts.include?(def_val) }
|
|
431
|
+
variants.map { |block| OneofCollector.collect(block) }
|
|
477
432
|
end
|
|
478
433
|
|
|
479
|
-
def validate(type, options, attrs,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
@api.
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def validate_value_coercion(coerce_type, *values_list)
|
|
492
|
-
return unless coerce_type
|
|
493
|
-
|
|
494
|
-
coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
|
|
495
|
-
values_list.each do |values|
|
|
496
|
-
next if !values || values.is_a?(Proc)
|
|
497
|
-
|
|
498
|
-
value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
|
|
499
|
-
value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
|
|
500
|
-
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
|
|
501
|
-
end
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
def extract_message_option(attrs)
|
|
505
|
-
return nil unless attrs.is_a?(Array)
|
|
506
|
-
|
|
507
|
-
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
|
508
|
-
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
def options_key?(type, key, validations)
|
|
512
|
-
validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
|
|
434
|
+
def validate(type, options, attrs, required, opts)
|
|
435
|
+
validator_class = Validations.require_validator(type)
|
|
436
|
+
validator_instance = validator_class.new(
|
|
437
|
+
attrs,
|
|
438
|
+
options,
|
|
439
|
+
required,
|
|
440
|
+
self,
|
|
441
|
+
opts
|
|
442
|
+
)
|
|
443
|
+
@api.inheritable_setting.add_validation(validator_instance)
|
|
513
444
|
end
|
|
514
445
|
|
|
515
446
|
def all_element_blank?(scoped_params)
|
|
516
447
|
scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
|
|
517
448
|
end
|
|
518
|
-
|
|
519
|
-
# Validators don't have access to each other and they don't need, however,
|
|
520
|
-
# some validators might influence others, so their options should be shared
|
|
521
|
-
def derive_validator_options(validations)
|
|
522
|
-
allow_blank = validations[:allow_blank]
|
|
523
|
-
|
|
524
|
-
{
|
|
525
|
-
allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
|
|
526
|
-
fail_fast: validations.delete(:fail_fast) || false
|
|
527
|
-
}
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
def validates_presence(validations, attrs, doc, opts)
|
|
531
|
-
return unless validations.key?(:presence) && validations[:presence]
|
|
532
|
-
|
|
533
|
-
validate('presence', validations.delete(:presence), attrs, doc, opts)
|
|
534
|
-
validations.delete(:message) if validations.key?(:message)
|
|
535
|
-
end
|
|
536
449
|
end
|
|
537
450
|
end
|
|
538
451
|
end
|