grape 4.0.0 → 4.1.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -1
  3. data/README.md +195 -95
  4. data/UPGRADING.md +364 -0
  5. data/grape.gemspec +1 -1
  6. data/lib/grape/api/instance.rb +57 -30
  7. data/lib/grape/api.rb +52 -18
  8. data/lib/grape/content_types.rb +26 -9
  9. data/lib/grape/cookies.rb +1 -1
  10. data/lib/grape/declared_params_handler.rb +35 -7
  11. data/lib/grape/dsl/declared.rb +5 -4
  12. data/lib/grape/dsl/desc.rb +1 -2
  13. data/lib/grape/dsl/entity.rb +35 -19
  14. data/lib/grape/dsl/headers.rb +3 -3
  15. data/lib/grape/dsl/helpers.rb +5 -8
  16. data/lib/grape/dsl/inside_route.rb +33 -27
  17. data/lib/grape/dsl/logger.rb +10 -4
  18. data/lib/grape/dsl/middleware.rb +4 -9
  19. data/lib/grape/dsl/mounting.rb +149 -0
  20. data/lib/grape/dsl/parameters.rb +78 -104
  21. data/lib/grape/dsl/request_response.rb +35 -22
  22. data/lib/grape/dsl/routing.rb +17 -191
  23. data/lib/grape/dsl/settings.rb +1 -1
  24. data/lib/grape/dsl/version_options.rb +2 -2
  25. data/lib/grape/dsl/versioning.rb +98 -0
  26. data/lib/grape/endpoint/state.rb +15 -0
  27. data/lib/grape/endpoint.rb +229 -137
  28. data/lib/grape/error_formatter/base.rb +28 -13
  29. data/lib/grape/error_formatter/json.rb +3 -0
  30. data/lib/grape/error_formatter/jsonl.rb +15 -0
  31. data/lib/grape/error_formatter/xml.rb +3 -1
  32. data/lib/grape/exceptions/error_response.rb +23 -7
  33. data/lib/grape/exceptions/halt.rb +39 -0
  34. data/lib/grape/exceptions/validation.rb +9 -9
  35. data/lib/grape/exceptions/validation_errors.rb +12 -2
  36. data/lib/grape/formatter/jsonl.rb +34 -0
  37. data/lib/grape/formatter/markdown.rb +17 -0
  38. data/lib/grape/formatter/md.rb +11 -0
  39. data/lib/grape/locale/en.yml +1 -0
  40. data/lib/grape/middleware/auth/base.rb +16 -1
  41. data/lib/grape/middleware/auth/strategy_info.rb +2 -2
  42. data/lib/grape/middleware/base.rb +19 -13
  43. data/lib/grape/middleware/error.rb +133 -35
  44. data/lib/grape/middleware/formatter.rb +235 -94
  45. data/lib/grape/middleware/precomputed_content_types.rb +14 -2
  46. data/lib/grape/middleware/stack.rb +7 -8
  47. data/lib/grape/middleware/versioner/base.rb +26 -10
  48. data/lib/grape/middleware/versioner/header.rb +70 -15
  49. data/lib/grape/middleware/versioner/param.rb +3 -5
  50. data/lib/grape/middleware/versioner/path.rb +20 -11
  51. data/lib/grape/middleware/versioner.rb +1 -1
  52. data/lib/grape/mountable.rb +2 -4
  53. data/lib/grape/namespace.rb +2 -3
  54. data/lib/grape/parser/jsonl.rb +19 -0
  55. data/lib/grape/request.rb +46 -8
  56. data/lib/grape/router/base_route.rb +41 -0
  57. data/lib/grape/router/greedy_route.rb +1 -1
  58. data/lib/grape/router/mustermann_pattern.rb +1 -1
  59. data/lib/grape/router/pattern/path.rb +1 -1
  60. data/lib/grape/router/pattern.rb +11 -5
  61. data/lib/grape/router/route.rb +42 -11
  62. data/lib/grape/router/route_buckets.rb +246 -0
  63. data/lib/grape/router.rb +262 -32
  64. data/lib/grape/util/endpoint_configuration.rb +7 -1
  65. data/lib/grape/util/inheritable_setting.rb +80 -161
  66. data/lib/grape/util/lazy/value.rb +52 -11
  67. data/lib/grape/util/media_type.rb +38 -15
  68. data/lib/grape/util/path_normalizer.rb +7 -2
  69. data/lib/grape/util/registry.rb +9 -2
  70. data/lib/grape/util/scrub.rb +33 -0
  71. data/lib/grape/util/translation.rb +6 -6
  72. data/lib/grape/validations/attributes_iterator.rb +73 -37
  73. data/lib/grape/validations/coerce_options.rb +1 -1
  74. data/lib/grape/validations/params_documentation.rb +15 -7
  75. data/lib/grape/validations/params_scope.rb +176 -43
  76. data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
  77. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  78. data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
  79. data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
  80. data/lib/grape/validations/types/primitive_coercer.rb +17 -0
  81. data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
  82. data/lib/grape/validations/types.rb +12 -18
  83. data/lib/grape/validations/validations_spec.rb +53 -22
  84. data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
  85. data/lib/grape/validations/validators/as_validator.rb +2 -3
  86. data/lib/grape/validations/validators/base.rb +124 -25
  87. data/lib/grape/validations/validators/coerce_validator.rb +2 -3
  88. data/lib/grape/validations/validators/default_validator.rb +18 -2
  89. data/lib/grape/validations/validators/except_values_validator.rb +16 -3
  90. data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
  91. data/lib/grape/validations/validators/length_validator.rb +24 -16
  92. data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
  93. data/lib/grape/validations/validators/values_validator.rb +18 -24
  94. data/lib/grape/version.rb +1 -2
  95. data/lib/grape.rb +3 -0
  96. metadata +18 -11
  97. data/lib/grape/util/lazy/value_array.rb +0 -21
  98. data/lib/grape/util/lazy/value_enumerable.rb +0 -31
  99. data/lib/grape/util/lazy/value_hash.rb +0 -21
  100. data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
  101. data/lib/grape/validations/single_attribute_iterator.rb +0 -28
@@ -2,37 +2,76 @@
2
2
 
3
3
  module Grape
4
4
  module Validations
5
+ # Yields each value a scope's attributes are read from: the scope's params,
6
+ # or each element of them when the scope sits in an Array. Walking the
7
+ # attributes themselves is left to the validator's block.
8
+ #
9
+ # Each value comes with whether it is an element a request sent in the
10
+ # scope's own Array. Otherwise it is the scope's value in an element of an
11
+ # Array further up, where an empty one is an optional scope left out of
12
+ # that element, or an element of an optional scope's Array whose elements
13
+ # are all empty, which is passed over the same way.
5
14
  class AttributesIterator
6
- # +attrs+ and +scope+ are static per validator; only +params+ varies
7
- # per request, so an instance can be built once and reused (it keeps
8
- # no request-derived state). Reused instances are shared across
9
- # threads, so +each+ must stay free of mutable instance state.
10
- def initialize(attrs, scope)
11
- @attrs = attrs
15
+ # +scope+ is static per validator; only +params+ varies per request, so
16
+ # an instance can be built once and reused (it keeps no request-derived
17
+ # state). Reused instances are shared across threads, so +each+ must
18
+ # stay free of mutable instance state.
19
+ def initialize(scope)
12
20
  @scope = scope
13
21
  # How many times #do_each may descend into a nested array. The
14
22
  # declaration allows one level per Array-typed scope on the chain, less
15
23
  # the one +Array.wrap+ already consumes in #each. Anything deeper was
16
24
  # put there by the request, not by the declaration.
17
25
  @max_nesting = [scope.array_depth - 1, 0].max
26
+ @iterates_elements = scope.iterates_elements?
27
+ @optional = !scope.required?
18
28
  end
19
29
 
20
30
  def each(params, &)
21
31
  original_params = @scope.params(params)
32
+ iterates_elements = @scope.iterates_elements?
22
33
  # A scope resolves to a Hash unless the declaration nests arrays, and
23
34
  # then #do_each has nothing to do but hand it straight back: Array.wrap
24
35
  # boxes it, the loop unboxes it on its only iteration, and with no Array
25
36
  # anywhere neither the nesting descent nor the index bookkeeping
26
37
  # applies. Every validator on a flat +params+ block comes through here.
27
- return yield_attributes(original_params, &) if original_params.is_a?(Hash) && !@scope.iterates_elements?
38
+ return yield(original_params) if original_params.is_a?(Hash) && !iterates_elements
39
+
40
+ array_params = original_params.is_a?(Array)
41
+ # Do not validate the content of an array scope that did not get one.
42
+ return if iterates_elements && !array_params
43
+
44
+ index_scope = index_scope_for(iterates_elements, array_params)
45
+ # No tracker means we're outside a ParamScopeTracker.track block (e.g.
46
+ # a unit test that invokes a validator directly). Index tracking is
47
+ # skipped — full_name will produce bracket-less names — but validation
48
+ # continues rather than crashing.
49
+ tracker = ParamScopeTracker.current if index_scope
28
50
 
29
51
  # because we need recursion for nested arrays
30
- do_each(Array.wrap(original_params), original_params, &)
52
+ params_to_process = Array.wrap(original_params)
53
+ do_each(params_to_process, tracker, index_scope, NO_PARENT_INDICES, sent_elements?(params_to_process, NO_PARENT_INDICES), &)
31
54
  end
32
55
 
33
56
  private
34
57
 
35
- def do_each(params_to_process, original_params, parent_indices = [], &block)
58
+ # The top-level call's parent indices; only ever read.
59
+ NO_PARENT_INDICES = [].freeze
60
+ private_constant :NO_PARENT_INDICES
61
+
62
+ # Where each element's index is recorded depends on the scope and on
63
+ # whether its params are an Array, never on the element, so it is
64
+ # settled once per #each rather than per element. A lateral scope (no
65
+ # @element) whose params resolved to an array hands its index to the
66
+ # nearest element-iterating ancestor, so full_name still produces the
67
+ # right bracketed index.
68
+ def index_scope_for(iterates_elements, array_params)
69
+ return @scope if iterates_elements
70
+
71
+ @scope.nearest_array_ancestor if array_params
72
+ end
73
+
74
+ def do_each(params_to_process, tracker, index_scope, parent_indices, sent_elements, &)
36
75
  params_to_process.each_with_index do |resource_params, index|
37
76
  # when we get arrays of arrays it means that target element located inside array
38
77
  # we need this because we want to know parent arrays indices
@@ -42,52 +81,49 @@ module Grape
42
81
  # validators see a non-hash and fail it the same way any other
43
82
  # unexpected element type does.
44
83
  if resource_params.is_a?(Array) && parent_indices.size < @max_nesting
45
- do_each(resource_params, original_params, [index] + parent_indices, &block)
84
+ nested_indices = [index] + parent_indices
85
+ do_each(resource_params, tracker, index_scope, nested_indices, sent_elements?(resource_params, nested_indices), &)
46
86
  next
47
87
  end
48
88
 
49
- if @scope.iterates_elements?
50
- next unless original_params.is_a?(Array) # do not validate content of array if it isn't array
89
+ store_indices(tracker, index_scope, index, parent_indices) if tracker
90
+ yield resource_params, sent_elements unless skip?(resource_params)
91
+ end
92
+ end
51
93
 
52
- store_indices(@scope, index, parent_indices)
53
- elsif original_params.is_a?(Array)
54
- # Lateral scope (no @element) whose params resolved to an array —
55
- # delegate index tracking to the nearest element-iterating ancestor
56
- # so that full_name produces the correct bracketed index.
57
- target = @scope.nearest_array_ancestor
58
- store_indices(target, index, parent_indices) if target
59
- end
94
+ # Whether +values+, reached through +parent_indices+, are elements of the
95
+ # scope's own Array that were sent. An optional scope's Array whose
96
+ # elements are all empty is passed over, as the scope itself is when it
97
+ # is left out (see ParamsScope#should_validate?, which answers that for
98
+ # the Array at the root). Checked once per Array rather than per element.
99
+ def sent_elements?(values, parent_indices)
100
+ @iterates_elements && parent_indices.size == @max_nesting && !(@optional && values.all? { |value| empty?(value) })
101
+ end
60
102
 
61
- yield_attributes(resource_params, &block)
62
- end
103
+ # Primitives like Integers and Booleans don't respond to +empty?+.
104
+ def empty?(value)
105
+ value.respond_to?(:empty?) ? value.empty? : value.nil?
63
106
  end
64
107
 
65
- def store_indices(target_scope, index, parent_indices)
66
- # No tracker means we're outside a ParamScopeTracker.track block (e.g.
67
- # a unit test that invokes a validator directly). Index tracking is
68
- # skipped — full_name will produce bracket-less names — but validation
69
- # continues rather than crashing.
70
- tracker = ParamScopeTracker.current or return
71
- parent_scope = target_scope.parent
108
+ # Each parent index belongs to the next element-iterating scope up the
109
+ # chain, which a Hash scope may sit below. There is always one:
110
+ # +parent_indices+ holds at most +array_depth - 1+ entries, and
111
+ # +target_scope+ has that many iterating ancestors.
112
+ def store_indices(tracker, target_scope, index, parent_indices)
113
+ parent_scope = target_scope.nearest_array_ancestor
72
114
  parent_indices.each do |parent_index|
73
- break unless parent_scope
74
-
75
115
  tracker.store_index(parent_scope, parent_index)
76
- parent_scope = parent_scope.parent
116
+ parent_scope = parent_scope.nearest_array_ancestor
77
117
  end
78
118
  tracker.store_index(target_scope, index)
79
119
  end
80
120
 
81
- def yield_attributes(_resource_params)
82
- raise NotImplementedError
83
- end
84
-
85
121
  # This is a special case so that we can ignore trees where option
86
122
  # values are missing lower down. Unfortunately we can't remove this
87
123
  # at the parameter parsing stage as they are required to ensure
88
124
  # the correct indexing is maintained
89
125
  def skip?(val)
90
- val == Grape::DSL::Parameters::EmptyOptionalValue
126
+ ParamsScope::EmptyOptionalValue.equal?(val)
91
127
  end
92
128
  end
93
129
  end
@@ -6,7 +6,7 @@ module Grape
6
6
  # by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/
7
7
  # +coerce_message+ declaration — never written by the user — and consumed
8
8
  # by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by
9
- # {Validators::Validators::CoerceValidator} (which receives it as its
9
+ # {Validators::CoerceValidator} (which receives it as its
10
10
  # +options+ argument).
11
11
  #
12
12
  # All three fields may be +nil+ (e.g. a remountable API evaluated on its
@@ -17,26 +17,34 @@ module Grape
17
17
  private
18
18
 
19
19
  def extract_details(spec)
20
- details = {}
21
- details[:required] = spec.required?
20
+ details = { required: spec.required? }
22
21
  details[:type] = TypeCache[spec.coerce_type] if spec.coerce_type
22
+ add_value_details(details, spec)
23
+ add_raw_details(details, spec.raw)
24
+ details
25
+ end
26
+
27
+ # The values the declaration accepts, rejects and defaults to.
28
+ def add_value_details(details, spec)
23
29
  details[:values] = spec.values if spec.values
24
30
  details[:except_values] = spec.except_values if spec.except_values
25
31
  details[:default] = spec.default unless spec.default.nil?
32
+ end
26
33
 
27
- length = spec.raw[:length]
34
+ # What the declaration's own options document beyond its validations:
35
+ # the +length+ bounds, the description and the +documentation+ Hash.
36
+ def add_raw_details(details, raw)
37
+ length = raw[:length]
28
38
  if length.is_a?(Hash)
29
39
  details[:min_length] = length[:min] if length.key?(:min)
30
40
  details[:max_length] = length[:max] if length.key?(:max)
31
41
  end
32
42
 
33
- desc = spec.raw[:desc] || spec.raw[:description]
43
+ desc = raw[:desc] || raw[:description]
34
44
  details[:desc] = desc if desc
35
45
 
36
- documentation = spec.raw[:documentation]
46
+ documentation = raw[:documentation]
37
47
  details[:documentation] = documentation if documentation
38
-
39
- details
40
48
  end
41
49
 
42
50
  class TypeCache < Grape::Util::Cache
@@ -5,10 +5,35 @@ module Grape
5
5
  class ParamsScope
6
6
  attr_reader :parent, :type, :nearest_array_ancestor, :array_depth, :full_path
7
7
 
8
+ # The elements a +given+ scope narrowed its Array params down to during
9
+ # this request (see #meets_dependency?). Only a scope with a dependency
10
+ # ever stores any, so every other scope answers without the fiber-storage
11
+ # and tracker lookups -- which #params pays on each nested resolution,
12
+ # twice per validator, on every request.
8
13
  def qualifying_params
14
+ return unless @dependent_on
15
+
9
16
  ParamScopeTracker.current&.qualifying_params(self)
10
17
  end
11
18
 
19
+ # @param params [Hash] initial hash of parameters
20
+ # @return hash of parameters relevant for the current scope
21
+ # @api private
22
+ # Only a +given+ scope ever stores qualifying params, and a scope on an
23
+ # independent chain has none above it (see #independent?), so the
24
+ # lookup and the +presence+ test that follows it are skipped -- both of
25
+ # them, at every level, on every call.
26
+ def params(params)
27
+ scoped = if @parent.nil?
28
+ params
29
+ elsif @independent
30
+ @parent.params(params)
31
+ else
32
+ @parent.qualifying_params.presence || @parent.params(params)
33
+ end
34
+ @element ? map_params(scoped, @element) : scoped
35
+ end
36
+
12
37
  include Grape::DSL::Parameters
13
38
  include Grape::Validations::ParamsDocumentation
14
39
 
@@ -19,6 +44,19 @@ module Grape
19
44
 
20
45
  SPECIAL_JSON = [JSON, Array[JSON]].freeze
21
46
 
47
+ # What {#validated_params} returns for a scope that should not be
48
+ # validated. A plain +false+ would be indistinguishable from params of
49
+ # that value, which +map_params+ can hand back for an element a request
50
+ # set to +false+.
51
+ NOT_VALIDATED = Object.new.freeze
52
+ private_constant :NOT_VALIDATED
53
+
54
+ # What {#params} hands back for an element of an Array that left an
55
+ # optional scope out, in place of that scope's params. Dropping the
56
+ # element instead would shift the indices the errors name, so it stays,
57
+ # and AttributesIterator and the validators pass over it.
58
+ EmptyOptionalValue = Object.new.freeze
59
+
22
60
  class Attr
23
61
  attr_reader :key, :scope
24
62
 
@@ -50,7 +88,7 @@ module Grape
50
88
  end
51
89
 
52
90
  # Open up a new ParamsScope, allowing parameter definitions per
53
- # Grape::DSL::Params.
91
+ # Grape::DSL::Parameters.
54
92
  # @param api [API] the API endpoint to modify
55
93
  # @param element [Symbol] the element that contains this scope; for
56
94
  # this to be relevant, parent must be set
@@ -59,10 +97,10 @@ module Grape
59
97
  # @param parent [ParamsScope] the scope containing this scope
60
98
  # @param optional [Boolean] whether or not this scope needs to have
61
99
  # any parameters set or not
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
65
- # validate if this param is present in the parent scope
100
+ # @param type [Class] a type meant to govern this scope
101
+ # @param group [Hash] group options for this scope
102
+ # @param dependent_on [Array<Symbol, Hash>] if present, this scope should
103
+ # only validate if these params are present in the parent scope
66
104
  # @yield the instance context, open for parameter definitions
67
105
  def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block)
68
106
  @element = element
@@ -73,6 +111,9 @@ module Grape
73
111
  @type = type
74
112
  @group = group
75
113
  @dependent_on = dependent_on
114
+ # See #independent?. Settled here because a scope's block may declare
115
+ # children, and each of them asks its parent.
116
+ @independent = !dependent_on && (parent.nil? || parent.independent?)
76
117
  # Must be an ivar: push_declared_params is dispatched on self during
77
118
  # instance_eval, so local variables from initialize are unreachable.
78
119
  # configure_declared_params consumes it and clears @declared_params to nil.
@@ -97,23 +138,35 @@ module Grape
97
138
  # @return [Boolean] whether or not this entire scope needs to be
98
139
  # validated
99
140
  def should_validate?(parameters)
141
+ return !NOT_VALIDATED.equal?(validated_params(parameters)) if @independent
142
+
100
143
  scoped_params = params(parameters)
101
144
 
102
- return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
145
+ return false if @optional && (Grape::Util::Scrub.blank?(scoped_params) || all_element_blank?(scoped_params))
103
146
  return false unless meets_dependency?(scoped_params, parameters)
104
- return true if @parent.nil?
105
147
 
148
+ # Past the +@independent+ return, this scope or one above it is a
149
+ # +given+ scope, and a +given+ scope is always nested: there is a parent.
106
150
  @parent.should_validate?(parameters)
107
151
  end
108
152
 
153
+ # Whether this scope and every scope above it declare no dependency, so
154
+ # #should_validate? has nothing to ask but whether each of them was given
155
+ # params -- and +qualifying_params+, which only a +given+ scope ever
156
+ # stores, cannot stand in for any of them.
157
+ # @return [Boolean]
158
+ def independent?
159
+ @independent
160
+ end
161
+
109
162
  def meets_dependency?(params, request_params)
110
163
  return true unless @dependent_on
111
- return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
164
+ return false if @parent && !@parent.meets_dependency?(@parent.params(request_params), request_params)
112
165
 
113
166
  if params.is_a?(Array)
114
167
  filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
115
168
  ParamScopeTracker.current&.store_qualifying_params(self, filtered)
116
- return filtered.present?
169
+ return !filtered.empty?
117
170
  end
118
171
 
119
172
  meets_hash_dependency?(params)
@@ -121,7 +174,7 @@ module Grape
121
174
 
122
175
  def attr_meets_dependency?(params)
123
176
  return true unless @dependent_on
124
- return false if @parent.present? && !@parent.attr_meets_dependency?(params)
177
+ return false if @parent && !@parent.attr_meets_dependency?(params)
125
178
 
126
179
  meets_hash_dependency?(params)
127
180
  end
@@ -135,7 +188,7 @@ module Grape
135
188
  key, callable = dependency.first
136
189
  callable.call(params[key])
137
190
  else
138
- params[dependency].present?
191
+ !Grape::Util::Scrub.blank?(params[dependency])
139
192
  end
140
193
  end
141
194
  end
@@ -166,6 +219,23 @@ module Grape
166
219
  !@parent
167
220
  end
168
221
 
222
+ # Whether #should_validate? answers true for every request: this scope
223
+ # and each of its ancestors is required and depends on nothing, so
224
+ # neither the params nor the parent chain have anything to say.
225
+ # @return [Boolean]
226
+ def always_validated?
227
+ !@optional && validated_when_given?
228
+ end
229
+
230
+ # Whether #should_validate? has nothing to ask but whether this scope's
231
+ # own params were given: it depends on no other param, and every scope
232
+ # above it is always validated. A required scope like that is then
233
+ # always validated, an optional one whenever its params are there.
234
+ # @return [Boolean]
235
+ def validated_when_given?
236
+ !@dependent_on && (@parent.nil? || @parent.always_validated?)
237
+ end
238
+
169
239
  # A nested scope is contained in one of its parent's elements.
170
240
  # @return [Boolean] whether or not this scope is nested
171
241
  def nested?
@@ -198,6 +268,29 @@ module Grape
198
268
 
199
269
  protected
200
270
 
271
+ # This scope's params, once it and every scope above it has been found
272
+ # worth validating; NOT_VALIDATED as soon as one of them is not.
273
+ #
274
+ # Only for an independent chain (see #independent?), where every scope
275
+ # answers from its own params alone: each one is then resolved from the
276
+ # one above rather than from the root, so the walk costs one +map_params+
277
+ # per scope instead of one per scope per level. Nothing here has a side
278
+ # effect, so checking from the root down rather than from here up reaches
279
+ # the same answer.
280
+ def validated_params(parameters)
281
+ if @parent
282
+ scoped = @parent.validated_params(parameters)
283
+ return scoped if NOT_VALIDATED.equal?(scoped)
284
+
285
+ scoped = map_params(scoped, @element) if @element
286
+ else
287
+ scoped = parameters
288
+ end
289
+ return NOT_VALIDATED if @optional && (Grape::Util::Scrub.blank?(scoped) || all_element_blank?(scoped))
290
+
291
+ scoped
292
+ end
293
+
201
294
  # Adds a parameter declaration to our list of validations.
202
295
  # @param attrs [Array] (see Grape::DSL::Parameters#requires)
203
296
  # +declared_params_scope+ is the scope an attribute is recorded against.
@@ -214,6 +307,28 @@ module Grape
214
307
 
215
308
  private
216
309
 
310
+ def map_params(params, element, is_array: false)
311
+ case params
312
+ when Array
313
+ params.map do |el|
314
+ map_params(el, element, is_array: true)
315
+ end
316
+ when Hash
317
+ params[element] || missing_element(is_array)
318
+ when EmptyOptionalValue
319
+ EmptyOptionalValue
320
+ else
321
+ {}
322
+ end
323
+ end
324
+
325
+ # What a Hash without the element maps to: EmptyOptionalValue for an
326
+ # optional scope's element of an Array, which the validators pass over,
327
+ # and an empty Hash otherwise.
328
+ def missing_element(is_array)
329
+ @optional && is_array ? EmptyOptionalValue : {}
330
+ end
331
+
217
332
  def build_full_path
218
333
  return @parent.full_path + [@element] if nested?
219
334
  return @parent.full_path if lateral?
@@ -249,10 +364,7 @@ module Grape
249
364
 
250
365
  requires(field, **field_opts)
251
366
  end
252
- optional_fields.each do |field|
253
- field_opts = using[field]
254
- optional(field, **field_opts) if field_opts
255
- end
367
+ declare_optional_fields(optional_fields, using)
256
368
  end
257
369
 
258
370
  def require_optional_fields(context, using:, except: nil)
@@ -261,7 +373,13 @@ module Grape
261
373
  except_fields = Array.wrap(except)
262
374
  optional_fields.delete_if { |f| except_fields.include?(f) }
263
375
  end
264
- optional_fields.each do |field|
376
+ declare_optional_fields(optional_fields, using)
377
+ end
378
+
379
+ # Each of +fields+ as optional, with its options in +using+; one +using+
380
+ # does not hold is left out.
381
+ def declare_optional_fields(fields, using)
382
+ fields.each do |field|
265
383
  field_opts = using[field]
266
384
  optional(field, **field_opts) if field_opts
267
385
  end
@@ -277,13 +395,15 @@ module Grape
277
395
  # @yield parameter scope
278
396
  def new_scope(element, type:, as:, optional: false, &)
279
397
  # 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
284
- raise Grape::Exceptions::MissingGroupType if type.nil?
285
- raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
286
- end
398
+ # object or repeat in a list, and nothing else could decide it. Checked
399
+ # here for `requires` and `optional` alike — `new_scope` is only reached
400
+ # from their block branch.
401
+ raise Grape::Exceptions::MissingGroupType if type.nil?
402
+ raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
403
+
404
+ # The block declares the keys of each element, so an element has to be
405
+ # a Hash. +Array[JSON]+ checks that when it parses the elements.
406
+ validate('hash_elements', {}, [element], false, {}) if type == Array
287
407
 
288
408
  self.class.new(
289
409
  api: @api,
@@ -291,7 +411,7 @@ module Grape
291
411
  element_renamed: as,
292
412
  parent: self,
293
413
  optional:,
294
- type: type || Array,
414
+ type:,
295
415
  group: @group,
296
416
  &
297
417
  )
@@ -299,8 +419,8 @@ module Grape
299
419
 
300
420
  # Returns a new parameter scope, not nested under any current-level param
301
421
  # but instead at the same level as the current scope.
302
- # @param dependent_on [Symbol] if given, specifies that this scope should
303
- # only validate if this parameter from the above scope is present
422
+ # @param dependent_on [Array<Symbol, Hash>] if given, this scope only
423
+ # validates if these parameters from the above scope are present
304
424
  # @yield parameter scope
305
425
  def new_lateral_scope(dependent_on:, &)
306
426
  self.class.new(
@@ -348,6 +468,23 @@ module Grape
348
468
  (iterates_elements? ? 1 : 0) + (@parent&.array_depth || 0)
349
469
  end
350
470
 
471
+ def validates(attrs, validations, required: false)
472
+ spec = ValidationsSpec.from(declared_validations(validations, required))
473
+
474
+ document_params(attrs, spec)
475
+
476
+ # Presence runs first — `required` is forwarded to every subsequent
477
+ # validator (some short-circuit on it).
478
+ validate_presence(spec, attrs)
479
+
480
+ # Coerce runs second — later validators see the typed value.
481
+ validate_coerce(spec, attrs)
482
+
483
+ spec.validator_entries.each do |type, options|
484
+ validate(type, options, attrs, spec.required?, spec.shared_opts)
485
+ end
486
+ end
487
+
351
488
  # +required+ is the DSL's own signal — +requires+ passes true, +optional+
352
489
  # false. It used to travel as a +:presence+ key that +requires+ wrote into
353
490
  # the caller's option Hash, which is why a user-supplied +presence:+ was
@@ -359,27 +496,15 @@ module Grape
359
496
  # A +presence:+ supplied by an API still decides the outcome exactly as it
360
497
  # used to — deprecated rather than dropped, so nothing changes under an
361
498
  # API that relies on it until the key is ignored outright.
362
- def validates(attrs, validations, required: false)
499
+ def declared_validations(validations, required)
363
500
  if validations.key?(:presence)
364
501
  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.')
365
502
  end
366
503
 
367
504
  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)
370
-
371
- document_params(attrs, spec)
372
-
373
- # Presence runs first — `required` is forwarded to every subsequent
374
- # validator (some short-circuit on it).
375
- validate_presence(spec, attrs)
376
-
377
- # Coerce runs second — later validators see the typed value.
378
- validate_coerce(spec, attrs)
505
+ return declared unless declared.key?(:oneof)
379
506
 
380
- spec.validator_entries.each do |type, options|
381
- validate(type, options, attrs, spec.required?, spec.shared_opts)
382
- end
507
+ declared.merge(oneof: collected_oneof(declared))
383
508
  end
384
509
 
385
510
  # Enforce correct usage of :coerce_with on a CoerceOptions.
@@ -425,7 +550,7 @@ module Grape
425
550
  raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
426
551
 
427
552
  variants = validations[:oneof]
428
- raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
553
+ raise ArgumentError, 'oneof: must be a non-empty Array of blocks' if !variants.is_a?(Array) || variants.empty?
429
554
  raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
430
555
 
431
556
  variants.map { |block| OneofCollector.collect(block) }
@@ -443,8 +568,16 @@ module Grape
443
568
  @api.inheritable_setting.add_validation(validator_instance)
444
569
  end
445
570
 
571
+ # Only ever asked about params that are not blank (see #should_validate?
572
+ # and #validated_params), and a Hash that is not blank is never all
573
+ # blank: what +all?+ yields for it is a [key, value] pair, an Array of
574
+ # two, which +blank?+ is false for. Asking anyway walked the Hash and
575
+ # built one of those Arrays per entry, for every optional Hash scope of
576
+ # every request.
446
577
  def all_element_blank?(scoped_params)
447
- scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
578
+ return false if scoped_params.is_a?(::Hash)
579
+
580
+ scoped_params.respond_to?(:all?) && scoped_params.all? { |element| Grape::Util::Scrub.blank?(element) }
448
581
  end
449
582
  end
450
583
  end
@@ -10,7 +10,7 @@ module Grape
10
10
  # there are any problems parsing the string.
11
11
  #
12
12
  # Alternately an optional +method+ may be supplied (see the
13
- # +coerce_with+ option of {Grape::Dsl::Parameters#requires}).
13
+ # +coerce_with+ option of {Grape::DSL::Parameters#requires}).
14
14
  # This may be any class or object implementing +parse+ or +call+,
15
15
  # with the same contract as described above.
16
16
  #
@@ -53,8 +53,7 @@ module Grape
53
53
 
54
54
  # Coerces the given value.
55
55
  #
56
- # @param value [String] value to be coerced, in grape
57
- # this should always be a string.
56
+ # @param val [Object] value to be coerced
58
57
  # @return [Object] the coerced result
59
58
  def call(val)
60
59
  coerced_val = @method.call(val)
@@ -4,7 +4,7 @@ module Grape
4
4
  module Validations
5
5
  module Types
6
6
  # See {CustomTypeCoercer} for details on types
7
- # that will be supported by this by this coercer.
7
+ # that will be supported by this coercer.
8
8
  # This coercer works in the same way as +CustomTypeCoercer+
9
9
  # except that it expects to receive an array of strings to
10
10
  # coerce and will return an array (or optionally, a set)