grape 0.1.1 → 2.4.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.
Files changed (197) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1167 -0
  3. data/CONTRIBUTING.md +156 -0
  4. data/LICENSE +1 -1
  5. data/README.md +4192 -0
  6. data/UPGRADING.md +1697 -0
  7. data/grape.gemspec +28 -105
  8. data/grape.png +0 -0
  9. data/lib/grape/api/helpers.rb +9 -0
  10. data/lib/grape/api/instance.rb +247 -0
  11. data/lib/grape/api.rb +158 -194
  12. data/lib/grape/content_types.rb +31 -0
  13. data/lib/grape/cookies.rb +50 -0
  14. data/lib/grape/dry_types.rb +10 -0
  15. data/lib/grape/dsl/api.rb +17 -0
  16. data/lib/grape/dsl/callbacks.rb +69 -0
  17. data/lib/grape/dsl/configuration.rb +15 -0
  18. data/lib/grape/dsl/desc.rb +124 -0
  19. data/lib/grape/dsl/headers.rb +21 -0
  20. data/lib/grape/dsl/helpers.rb +108 -0
  21. data/lib/grape/dsl/inside_route.rb +454 -0
  22. data/lib/grape/dsl/logger.rb +22 -0
  23. data/lib/grape/dsl/middleware.rb +54 -0
  24. data/lib/grape/dsl/parameters.rb +266 -0
  25. data/lib/grape/dsl/request_response.rb +171 -0
  26. data/lib/grape/dsl/routing.rb +248 -0
  27. data/lib/grape/dsl/settings.rb +179 -0
  28. data/lib/grape/dsl/validations.rb +57 -0
  29. data/lib/grape/endpoint.rb +398 -68
  30. data/lib/grape/env.rb +20 -0
  31. data/lib/grape/error_formatter/base.rb +68 -0
  32. data/lib/grape/error_formatter/json.rb +28 -0
  33. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  34. data/lib/grape/error_formatter/txt.rb +21 -0
  35. data/lib/grape/error_formatter/xml.rb +11 -0
  36. data/lib/grape/error_formatter.rb +15 -0
  37. data/lib/grape/exceptions/base.rb +76 -0
  38. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  39. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +11 -0
  41. data/lib/grape/exceptions/invalid_accept_header.rb +11 -0
  42. data/lib/grape/exceptions/invalid_formatter.rb +11 -0
  43. data/lib/grape/exceptions/invalid_message_body.rb +11 -0
  44. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  45. data/lib/grape/exceptions/invalid_response.rb +11 -0
  46. data/lib/grape/exceptions/invalid_version_header.rb +11 -0
  47. data/lib/grape/exceptions/invalid_versioner_option.rb +11 -0
  48. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +11 -0
  49. data/lib/grape/exceptions/method_not_allowed.rb +11 -0
  50. data/lib/grape/exceptions/missing_group_type.rb +13 -0
  51. data/lib/grape/exceptions/missing_mime_type.rb +11 -0
  52. data/lib/grape/exceptions/missing_option.rb +11 -0
  53. data/lib/grape/exceptions/missing_vendor_option.rb +11 -0
  54. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  55. data/lib/grape/exceptions/too_many_multipart_files.rb +11 -0
  56. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  57. data/lib/grape/exceptions/unknown_options.rb +11 -0
  58. data/lib/grape/exceptions/unknown_parameter.rb +11 -0
  59. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  60. data/lib/grape/exceptions/unknown_validator.rb +11 -0
  61. data/lib/grape/exceptions/unsupported_group_type.rb +13 -0
  62. data/lib/grape/exceptions/validation.rb +25 -0
  63. data/lib/grape/exceptions/validation_array_errors.rb +14 -0
  64. data/lib/grape/exceptions/validation_errors.rb +53 -0
  65. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +24 -0
  66. data/lib/grape/extensions/hash.rb +27 -0
  67. data/lib/grape/extensions/hashie/mash.rb +24 -0
  68. data/lib/grape/formatter/base.rb +16 -0
  69. data/lib/grape/formatter/json.rb +13 -0
  70. data/lib/grape/formatter/serializable_hash.rb +39 -0
  71. data/lib/grape/formatter/txt.rb +11 -0
  72. data/lib/grape/formatter/xml.rb +13 -0
  73. data/lib/grape/formatter.rb +17 -0
  74. data/lib/grape/json.rb +10 -0
  75. data/lib/grape/locale/en.yml +59 -0
  76. data/lib/grape/middleware/auth/base.rb +23 -0
  77. data/lib/grape/middleware/auth/dsl.rb +39 -0
  78. data/lib/grape/middleware/auth/strategies.rb +25 -0
  79. data/lib/grape/middleware/auth/strategy_info.rb +15 -0
  80. data/lib/grape/middleware/base.rb +89 -18
  81. data/lib/grape/middleware/error.rb +129 -10
  82. data/lib/grape/middleware/filter.rb +19 -0
  83. data/lib/grape/middleware/formatter.rb +124 -82
  84. data/lib/grape/middleware/globals.rb +14 -0
  85. data/lib/grape/middleware/stack.rb +109 -0
  86. data/lib/grape/middleware/versioner/accept_version_header.rb +38 -0
  87. data/lib/grape/middleware/versioner/base.rb +74 -0
  88. data/lib/grape/middleware/versioner/header.rb +127 -0
  89. data/lib/grape/middleware/versioner/param.rb +32 -0
  90. data/lib/grape/middleware/versioner/path.rb +40 -0
  91. data/lib/grape/middleware/versioner.rb +21 -21
  92. data/lib/grape/namespace.rb +46 -0
  93. data/lib/grape/params_builder/base.rb +18 -0
  94. data/lib/grape/params_builder/hash.rb +11 -0
  95. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  96. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  97. data/lib/grape/params_builder.rb +32 -0
  98. data/lib/grape/parser/base.rb +16 -0
  99. data/lib/grape/parser/json.rb +14 -0
  100. data/lib/grape/parser/xml.rb +14 -0
  101. data/lib/grape/parser.rb +15 -0
  102. data/lib/grape/path.rb +76 -0
  103. data/lib/grape/presenters/presenter.rb +11 -0
  104. data/lib/grape/railtie.rb +9 -0
  105. data/lib/grape/request.rb +190 -0
  106. data/lib/grape/router/base_route.rb +39 -0
  107. data/lib/grape/router/greedy_route.rb +20 -0
  108. data/lib/grape/router/pattern.rb +81 -0
  109. data/lib/grape/router/route.rb +62 -0
  110. data/lib/grape/router.rb +190 -0
  111. data/lib/grape/serve_stream/file_body.rb +36 -0
  112. data/lib/grape/serve_stream/sendfile_response.rb +21 -0
  113. data/lib/grape/serve_stream/stream_response.rb +23 -0
  114. data/lib/grape/types/invalid_value.rb +8 -0
  115. data/lib/grape/util/base_inheritable.rb +43 -0
  116. data/lib/grape/util/cache.rb +17 -0
  117. data/lib/grape/util/endpoint_configuration.rb +8 -0
  118. data/lib/grape/util/header.rb +13 -0
  119. data/lib/grape/util/inheritable_setting.rb +100 -0
  120. data/lib/grape/util/inheritable_values.rb +29 -0
  121. data/lib/grape/util/lazy/block.rb +29 -0
  122. data/lib/grape/util/lazy/value.rb +38 -0
  123. data/lib/grape/util/lazy/value_array.rb +21 -0
  124. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  125. data/lib/grape/util/lazy/value_hash.rb +21 -0
  126. data/lib/grape/util/media_type.rb +70 -0
  127. data/lib/grape/util/registry.rb +27 -0
  128. data/lib/grape/util/reverse_stackable_values.rb +15 -0
  129. data/lib/grape/util/stackable_values.rb +36 -0
  130. data/lib/grape/util/strict_hash_configuration.rb +108 -0
  131. data/lib/grape/validations/attributes_doc.rb +60 -0
  132. data/lib/grape/validations/attributes_iterator.rb +62 -0
  133. data/lib/grape/validations/contract_scope.rb +34 -0
  134. data/lib/grape/validations/multiple_attributes_iterator.rb +13 -0
  135. data/lib/grape/validations/params_scope.rb +538 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +26 -0
  137. data/lib/grape/validations/types/array_coercer.rb +61 -0
  138. data/lib/grape/validations/types/custom_type_coercer.rb +164 -0
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +56 -0
  140. data/lib/grape/validations/types/dry_type_coercer.rb +66 -0
  141. data/lib/grape/validations/types/file.rb +31 -0
  142. data/lib/grape/validations/types/invalid_value.rb +17 -0
  143. data/lib/grape/validations/types/json.rb +71 -0
  144. data/lib/grape/validations/types/multiple_type_coercer.rb +57 -0
  145. data/lib/grape/validations/types/primitive_coercer.rb +73 -0
  146. data/lib/grape/validations/types/set_coercer.rb +35 -0
  147. data/lib/grape/validations/types/variant_collection_coercer.rb +51 -0
  148. data/lib/grape/validations/types.rb +213 -0
  149. data/lib/grape/validations/validator_factory.rb +15 -0
  150. data/lib/grape/validations/validators/all_or_none_of_validator.rb +16 -0
  151. data/lib/grape/validations/validators/allow_blank_validator.rb +20 -0
  152. data/lib/grape/validations/validators/as_validator.rb +14 -0
  153. data/lib/grape/validations/validators/at_least_one_of_validator.rb +15 -0
  154. data/lib/grape/validations/validators/base.rb +88 -0
  155. data/lib/grape/validations/validators/coerce_validator.rb +75 -0
  156. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  157. data/lib/grape/validations/validators/default_validator.rb +37 -0
  158. data/lib/grape/validations/validators/exactly_one_of_validator.rb +17 -0
  159. data/lib/grape/validations/validators/except_values_validator.rb +24 -0
  160. data/lib/grape/validations/validators/length_validator.rb +49 -0
  161. data/lib/grape/validations/validators/multiple_params_base.rb +34 -0
  162. data/lib/grape/validations/validators/mutual_exclusion_validator.rb +16 -0
  163. data/lib/grape/validations/validators/presence_validator.rb +15 -0
  164. data/lib/grape/validations/validators/regexp_validator.rb +16 -0
  165. data/lib/grape/validations/validators/same_as_validator.rb +29 -0
  166. data/lib/grape/validations/validators/values_validator.rb +60 -0
  167. data/lib/grape/validations.rb +21 -0
  168. data/lib/grape/version.rb +6 -0
  169. data/lib/grape/xml.rb +10 -0
  170. data/lib/grape.rb +81 -17
  171. metadata +250 -192
  172. data/.document +0 -5
  173. data/.gitignore +0 -23
  174. data/.rspec +0 -2
  175. data/.rvmrc +0 -1
  176. data/Gemfile +0 -21
  177. data/Gemfile.lock +0 -58
  178. data/README.markdown +0 -75
  179. data/Rakefile +0 -70
  180. data/VERSION +0 -1
  181. data/autotest/discover.rb +0 -1
  182. data/lib/grape/middleware/auth/basic.rb +0 -30
  183. data/lib/grape/middleware/auth/oauth2.rb +0 -55
  184. data/lib/grape/middleware/prefixer.rb +0 -21
  185. data/lib/grape/middleware_stack.rb +0 -35
  186. data/spec/grape/api_spec.rb +0 -343
  187. data/spec/grape/endpoint_spec.rb +0 -104
  188. data/spec/grape/middleware/auth/basic_spec.rb +0 -31
  189. data/spec/grape/middleware/auth/oauth2_spec.rb +0 -88
  190. data/spec/grape/middleware/base_spec.rb +0 -63
  191. data/spec/grape/middleware/error_spec.rb +0 -49
  192. data/spec/grape/middleware/formatter_spec.rb +0 -87
  193. data/spec/grape/middleware/prefixer_spec.rb +0 -30
  194. data/spec/grape/middleware/versioner_spec.rb +0 -40
  195. data/spec/grape/middleware_stack_spec.rb +0 -47
  196. data/spec/grape_spec.rb +0 -1
  197. data/spec/spec_helper.rb +0 -20
@@ -0,0 +1,538 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ class ParamsScope
6
+ attr_accessor :element, :parent, :index
7
+ attr_reader :type, :params_meeting_dependency
8
+
9
+ include Grape::DSL::Parameters
10
+
11
+ # There are a number of documentation options on entities that don't have
12
+ # corresponding validators. Since there is nowhere that enumerates them all,
13
+ # we maintain a list of them here and skip looking up validators for them.
14
+ RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
15
+
16
+ class Attr
17
+ attr_accessor :key, :scope
18
+
19
+ # Open up a new ParamsScope::Attr
20
+ # @param key [Hash, Symbol] key of attr
21
+ # @param scope [Grape::Validations::ParamsScope] scope of attr
22
+ def initialize(key, scope)
23
+ @key = key
24
+ @scope = scope
25
+ end
26
+
27
+ # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr
28
+ def self.attrs_keys(declared_params)
29
+ declared_params.map do |declared_param_attr|
30
+ attr_key(declared_param_attr)
31
+ end
32
+ end
33
+
34
+ def self.attr_key(declared_param_attr)
35
+ return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
36
+
37
+ if declared_param_attr.is_a?(Hash)
38
+ declared_param_attr.transform_values { |value| attrs_keys(value) }
39
+ else
40
+ declared_param_attr
41
+ end
42
+ end
43
+ end
44
+
45
+ # Open up a new ParamsScope, allowing parameter definitions per
46
+ # Grape::DSL::Params.
47
+ # @param opts [Hash] options for this scope
48
+ # @option opts :element [Symbol] the element that contains this scope; for
49
+ # this to be relevant, @parent must be set
50
+ # @option opts :element_renamed [Symbol, nil] whenever this scope should
51
+ # be renamed and to what, given +nil+ no renaming is done
52
+ # @option opts :parent [ParamsScope] the scope containing this scope
53
+ # @option opts :api [API] the API endpoint to modify
54
+ # @option opts :optional [Boolean] whether or not this scope needs to have
55
+ # any parameters set or not
56
+ # @option opts :type [Class] a type meant to govern this scope (deprecated)
57
+ # @option opts :type [Hash] group options for this scope
58
+ # @option opts :dependent_on [Symbol] if present, this scope should only
59
+ # validate if this param is present in the parent scope
60
+ # @yield the instance context, open for parameter definitions
61
+ def initialize(opts, &block)
62
+ @element = opts[:element]
63
+ @element_renamed = opts[:element_renamed]
64
+ @parent = opts[:parent]
65
+ @api = opts[:api]
66
+ @optional = opts[:optional] || false
67
+ @type = opts[:type]
68
+ @group = opts[:group]
69
+ @dependent_on = opts[:dependent_on]
70
+ @params_meeting_dependency = []
71
+ @declared_params = []
72
+ @index = nil
73
+
74
+ instance_eval(&block) if block
75
+
76
+ configure_declared_params
77
+ end
78
+
79
+ def configuration
80
+ @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
81
+ end
82
+
83
+ # @return [Boolean] whether or not this entire scope needs to be
84
+ # validated
85
+ def should_validate?(parameters)
86
+ scoped_params = params(parameters)
87
+
88
+ return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
89
+ return false unless meets_dependency?(scoped_params, parameters)
90
+ return true if parent.nil?
91
+
92
+ parent.should_validate?(parameters)
93
+ end
94
+
95
+ def meets_dependency?(params, request_params)
96
+ return true unless @dependent_on
97
+ return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
98
+
99
+ if params.is_a?(Array)
100
+ @params_meeting_dependency = params.flatten.filter { |param| meets_dependency?(param, request_params) }
101
+ return @params_meeting_dependency.present?
102
+ end
103
+
104
+ meets_hash_dependency?(params)
105
+ end
106
+
107
+ def attr_meets_dependency?(params)
108
+ return true unless @dependent_on
109
+ return false if @parent.present? && !@parent.attr_meets_dependency?(params)
110
+
111
+ meets_hash_dependency?(params)
112
+ end
113
+
114
+ def meets_hash_dependency?(params)
115
+ # params might be anything what looks like a hash, so it must implement a `key?` method
116
+ return false unless params.respond_to?(:key?)
117
+
118
+ @dependent_on.each do |dependency|
119
+ if dependency.is_a?(Hash)
120
+ dependency_key = dependency.keys[0]
121
+ proc = dependency.values[0]
122
+ return false unless proc.call(params.try(:[], dependency_key))
123
+ elsif params.respond_to?(:key?) && params.try(:[], dependency).blank?
124
+ return false
125
+ end
126
+ end
127
+
128
+ true
129
+ end
130
+
131
+ # @return [String] the proper attribute name, with nesting considered.
132
+ def full_name(name, index: nil)
133
+ if nested?
134
+ # Find our containing element's name, and append ours.
135
+ "#{@parent.full_name(@element)}#{brackets(index || @index)}#{brackets(name)}"
136
+ elsif lateral?
137
+ # Find the name of the element as if it was at the same nesting level
138
+ # as our parent. We need to forward our index upward to achieve this.
139
+ @parent.full_name(name, index: @index)
140
+ else
141
+ # We must be the root scope, so no prefix needed.
142
+ name.to_s
143
+ end
144
+ end
145
+
146
+ def brackets(val)
147
+ "[#{val}]" if val
148
+ end
149
+
150
+ # @return [Boolean] whether or not this scope is the root-level scope
151
+ def root?
152
+ !@parent
153
+ end
154
+
155
+ # A nested scope is contained in one of its parent's elements.
156
+ # @return [Boolean] whether or not this scope is nested
157
+ def nested?
158
+ @parent && @element
159
+ end
160
+
161
+ # A lateral scope is subordinate to its parent, but its keys are at the
162
+ # same level as its parent and thus is not contained within an element.
163
+ # @return [Boolean] whether or not this scope is lateral
164
+ def lateral?
165
+ @parent && !@element
166
+ end
167
+
168
+ # @return [Boolean] whether or not this scope needs to be present, or can
169
+ # be blank
170
+ def required?
171
+ !@optional
172
+ end
173
+
174
+ def reset_index
175
+ @index = nil
176
+ end
177
+
178
+ protected
179
+
180
+ # Adds a parameter declaration to our list of validations.
181
+ # @param attrs [Array] (see Grape::DSL::Parameters#requires)
182
+ def push_declared_params(attrs, opts = {})
183
+ opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope)
184
+ return @parent.push_declared_params(attrs, opts) if lateral?
185
+
186
+ push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as]
187
+ @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
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
201
+ end
202
+
203
+ private
204
+
205
+ # Add a new parameter which should be renamed when using the +#declared+
206
+ # method.
207
+ #
208
+ # @param path [Array<String, Symbol>] the full path of the parameter
209
+ # (including the parameter name as last array element)
210
+ # @param new_name [String, Symbol] the new name of the parameter (the
211
+ # renamed name, with the +as: ...+ semantic)
212
+ def push_renamed_param(path, new_name)
213
+ base = @api.route_setting(:renamed_params) || {}
214
+ base[Array(path).map(&:to_s)] = new_name.to_s
215
+ @api.route_setting(:renamed_params, base)
216
+ end
217
+
218
+ def require_required_and_optional_fields(context, opts)
219
+ if context == :all
220
+ optional_fields = Array.wrap(opts[:except])
221
+ required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) }
222
+ else # context == :none
223
+ required_fields = Array.wrap(opts[:except])
224
+ optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) }
225
+ end
226
+ required_fields.each do |field|
227
+ field_opts = opts[:using][field]
228
+ raise ArgumentError, "required field not exist: #{field}" unless field_opts
229
+
230
+ requires(field, field_opts)
231
+ end
232
+ optional_fields.each do |field|
233
+ field_opts = opts[:using][field]
234
+ optional(field, field_opts) if field_opts
235
+ end
236
+ end
237
+
238
+ def require_optional_fields(context, opts)
239
+ optional_fields = opts[:using].keys
240
+ unless context == :all
241
+ except_fields = Array.wrap(opts[:except])
242
+ optional_fields.delete_if { |f| except_fields.include?(f) }
243
+ end
244
+ optional_fields.each do |field|
245
+ field_opts = opts[:using][field]
246
+ optional(field, field_opts) if field_opts
247
+ end
248
+ end
249
+
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
+ # Returns a new parameter scope, subordinate to the current one and nested
257
+ # under the parameter corresponding to `attrs.first`.
258
+ # @param attrs [Array] the attributes passed to the `requires` or
259
+ # `optional` invocation that opened this scope.
260
+ # @param optional [Boolean] whether the parameter this are nested under
261
+ # is optional or not (and hence, whether this block's params will be).
262
+ # @yield parameter scope
263
+ def new_scope(attrs, optional = false, &block)
264
+ # if required params are grouped and no type or unsupported type is provided, raise an error
265
+ type = attrs[1] ? attrs[1][:type] : nil
266
+ if attrs.first && !optional
267
+ raise Grape::Exceptions::MissingGroupType if type.nil?
268
+ raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
269
+ end
270
+
271
+ self.class.new(
272
+ api: @api,
273
+ element: attrs.first,
274
+ element_renamed: attrs[1][:as],
275
+ parent: self,
276
+ optional: optional,
277
+ type: type || Array,
278
+ group: @group,
279
+ &block
280
+ )
281
+ end
282
+
283
+ # Returns a new parameter scope, not nested under any current-level param
284
+ # but instead at the same level as the current scope.
285
+ # @param options [Hash] options to control how this new scope behaves
286
+ # @option options :dependent_on [Symbol] if given, specifies that this
287
+ # scope should only validate if this parameter from the above scope is
288
+ # present
289
+ # @yield parameter scope
290
+ def new_lateral_scope(options, &block)
291
+ self.class.new(
292
+ api: @api,
293
+ element: nil,
294
+ parent: self,
295
+ options: @optional,
296
+ type: type == Array ? Array : Hash,
297
+ dependent_on: options[:dependent_on],
298
+ &block
299
+ )
300
+ end
301
+
302
+ # Returns a new parameter scope, subordinate to the current one and nested
303
+ # under the parameter corresponding to `attrs.first`.
304
+ # @param attrs [Array] the attributes passed to the `requires` or
305
+ # `optional` invocation that opened this scope.
306
+ # @yield parameter scope
307
+ def new_group_scope(attrs, &block)
308
+ self.class.new(api: @api, parent: self, group: attrs.first, &block)
309
+ end
310
+
311
+ # Pushes declared params to parent or settings
312
+ def configure_declared_params
313
+ push_renamed_param(full_path, @element_renamed) if @element_renamed
314
+
315
+ if nested?
316
+ @parent.push_declared_params [element => @declared_params]
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
322
+ @declared_params = nil
323
+ end
324
+
325
+ def validates(attrs, validations)
326
+ doc = AttributesDoc.new @api, self
327
+ doc.extract_details validations
328
+
329
+ coerce_type = infer_coercion(validations)
330
+
331
+ doc.type = coerce_type
332
+
333
+ default = validations[:default]
334
+
335
+ if (values_hash = validations[:values]).is_a? Hash
336
+ values = values_hash[:value]
337
+ # NB: excepts is deprecated
338
+ excepts = values_hash[:except]
339
+ else
340
+ values = validations[:values]
341
+ end
342
+
343
+ doc.values = values
344
+
345
+ except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
346
+
347
+ # NB. values and excepts should be nil, Proc, Array, or Range.
348
+ # Specifically, values should NOT be a Hash
349
+
350
+ # use values or excepts to guess coerce type when stated type is Array
351
+ coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts)
352
+
353
+ # default value should be present in values array, if both exist and are not procs
354
+ check_incompatible_option_values(default, values, except_values, excepts)
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
+ )
411
+ end
412
+ validations.delete(:type)
413
+
414
+ coerce_type
415
+ end
416
+
417
+ # Enforce correct usage of :coerce_with parameter.
418
+ # We do not allow coercion without a type, nor with
419
+ # +JSON+ as a type since this defines its own coercion
420
+ # method.
421
+ def check_coerce_with(validations)
422
+ return unless validations.key?(:coerce_with)
423
+ # type must be supplied for coerce_with..
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]
429
+
430
+ raise ArgumentError, 'coerce_with disallowed for type: JSON'
431
+ end
432
+
433
+ # Add type coercion validation to this scope,
434
+ # if any has been specified.
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)
441
+
442
+ return unless validations.key?(:coerce)
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)
453
+ end
454
+
455
+ def guess_coerce_type(coerce_type, *values_list)
456
+ return coerce_type unless coerce_type == Array
457
+
458
+ values_list.each do |values|
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
463
+ end
464
+
465
+ def check_incompatible_option_values(default, values, except_values, excepts)
466
+ return unless default && !default.is_a?(Proc)
467
+
468
+ 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
+
470
+ if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
471
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
472
+ end
473
+
474
+ return unless excepts && !excepts.is_a?(Proc)
475
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
476
+ unless Array(default).none? { |def_val| excepts.include?(def_val) }
477
+ end
478
+
479
+ def validate(type, options, attrs, doc, opts)
480
+ validator_options = {
481
+ attributes: attrs,
482
+ options: options,
483
+ required: doc.required,
484
+ params_scope: self,
485
+ opts: opts,
486
+ validator_class: Validations.require_validator(type)
487
+ }
488
+ @api.namespace_stackable(:validations, validator_options)
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?
513
+ end
514
+
515
+ def all_element_blank?(scoped_params)
516
+ scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
517
+ 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
+ end
537
+ end
538
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ class SingleAttributeIterator < AttributesIterator
6
+ private
7
+
8
+ def yield_attributes(val, attrs)
9
+ return if skip?(val)
10
+
11
+ attrs.each do |attr_name|
12
+ yield val, attr_name, empty?(val)
13
+ end
14
+ end
15
+
16
+ # Primitives like Integers and Booleans don't respond to +empty?+.
17
+ # It could be possible to use +blank?+ instead, but
18
+ #
19
+ # false.blank?
20
+ # => true
21
+ def empty?(val)
22
+ val.respond_to?(:empty?) ? val.empty? : val.nil?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Coerces elements in an array. It might be an array of strings or integers or
7
+ # an array of arrays of integers.
8
+ #
9
+ # It could've been possible to use an +of+
10
+ # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
11
+ # provided by dry-types. Unfortunately, it doesn't work for Grape because of
12
+ # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
13
+ # maintains Virtus behavior in coercing.
14
+ class ArrayCoercer < DryTypeCoercer
15
+ def initialize(type, strict = false)
16
+ super
17
+
18
+ @coercer = scope::Array
19
+ @subtype = type.first
20
+ end
21
+
22
+ def call(_val)
23
+ collection = super
24
+ return collection if collection.is_a?(InvalidValue)
25
+
26
+ coerce_elements collection
27
+ end
28
+
29
+ protected
30
+
31
+ attr_reader :subtype
32
+
33
+ def coerce_elements(collection)
34
+ return if collection.nil?
35
+
36
+ collection.each_with_index do |elem, index|
37
+ return InvalidValue.new if reject?(elem)
38
+
39
+ coerced_elem = elem_coercer.call(elem)
40
+
41
+ return coerced_elem if coerced_elem.is_a?(InvalidValue)
42
+
43
+ collection[index] = coerced_elem
44
+ end
45
+
46
+ collection
47
+ end
48
+
49
+ # This method maintains logic which was defined by Virtus for arrays.
50
+ # Virtus doesn't allow nil in arrays.
51
+ def reject?(val)
52
+ val.nil?
53
+ end
54
+
55
+ def elem_coercer
56
+ @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end