• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ruby-grape / grape / 17881450095

20 Sep 2025 03:10PM UTC coverage: 96.666% (-1.5%) from 98.194%
17881450095

Pull #2605

github

ericproulx
Add Rack 3.2 support

- Add gemfiles/rack_3_2.gemfile for testing with Rack ~> 3.2
- Update GitHub Actions workflow to include rack_3_2.gemfile in test matrix
- Ensures compatibility with latest Rack 3.2.x versions
Pull Request #2605: Add Rack 3.2 Support

1072 of 1163 branches covered (92.18%)

Branch coverage included in aggregate %.

23 of 23 new or added lines in 7 files covered. (100.0%)

1 existing line in 1 file now uncovered.

3364 of 3426 relevant lines covered (98.19%)

55826.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.36
/lib/grape/validations/params_scope.rb
1
# frozen_string_literal: true
2

3
module Grape
49✔
4
  module Validations
49✔
5
    class ParamsScope
49✔
6
      attr_accessor :element, :parent, :index
49✔
7
      attr_reader :type, :params_meeting_dependency
49✔
8

9
      include Grape::DSL::Parameters
49✔
10
      include Grape::Validations::ParamsDocumentation
49✔
11

12
      # There are a number of documentation options on entities that don't have
13
      # corresponding validators. Since there is nowhere that enumerates them all,
14
      # we maintain a list of them here and skip looking up validators for them.
15
      RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
49✔
16

17
      class Attr
49✔
18
        attr_accessor :key, :scope
49✔
19

20
        # Open up a new ParamsScope::Attr
21
        # @param key [Hash, Symbol] key of attr
22
        # @param scope [Grape::Validations::ParamsScope] scope of attr
23
        def initialize(key, scope)
49✔
24
          @key = key
154,940✔
25
          @scope = scope
154,940✔
26
        end
27

28
        # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr
29
        def self.attrs_keys(declared_params)
49✔
30
          declared_params.map do |declared_param_attr|
840✔
31
            attr_key(declared_param_attr)
1,302✔
32
          end
33
        end
34

35
        def self.attr_key(declared_param_attr)
49✔
36
          return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
2,604✔
37

38
          if declared_param_attr.is_a?(Hash)
1,302✔
39
            declared_param_attr.transform_values { |value| attrs_keys(value) }
588✔
40
          else
41
            declared_param_attr
1,008✔
42
          end
43
        end
44
      end
45

46
      # Open up a new ParamsScope, allowing parameter definitions per
47
      #   Grape::DSL::Params.
48
      # @param opts [Hash] options for this scope
49
      # @option opts :element [Symbol] the element that contains this scope; for
50
      #   this to be relevant, @parent must be set
51
      # @option opts :element_renamed [Symbol, nil] whenever this scope should
52
      #   be renamed and to what, given +nil+ no renaming is done
53
      # @option opts :parent [ParamsScope] the scope containing this scope
54
      # @option opts :api [API] the API endpoint to modify
55
      # @option opts :optional [Boolean] whether or not this scope needs to have
56
      #   any parameters set or not
57
      # @option opts :type [Class] a type meant to govern this scope (deprecated)
58
      # @option opts :type [Hash] group options for this scope
59
      # @option opts :dependent_on [Symbol] if present, this scope should only
60
      #   validate if this param is present in the parent scope
61
      # @yield the instance context, open for parameter definitions
62
      def initialize(opts, &block)
49✔
63
        @element          = opts[:element]
119,634✔
64
        @element_renamed  = opts[:element_renamed]
119,634✔
65
        @parent           = opts[:parent]
119,634✔
66
        @api              = opts[:api]
119,634✔
67
        @optional         = opts[:optional] || false
119,634✔
68
        @type             = opts[:type]
119,634✔
69
        @group            = opts[:group]
119,634✔
70
        @dependent_on     = opts[:dependent_on]
119,634✔
71
        @params_meeting_dependency = []
119,634✔
72
        @declared_params = []
119,634✔
73
        @index = nil
119,634✔
74

75
        instance_eval(&block) if block
119,634✔
76

77
        configure_declared_params
118,584✔
78
      end
79

80
      def configuration
49✔
81
        @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
630✔
82
      end
83

84
      # @return [Boolean] whether or not this entire scope needs to be
85
      #   validated
86
      def should_validate?(parameters)
49✔
87
        scoped_params = params(parameters)
208,227✔
88

89
        return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
208,227✔
90
        return false unless meets_dependency?(scoped_params, parameters)
199,945✔
91
        return true if parent.nil?
182,557✔
92

93
        parent.should_validate?(parameters)
61,530✔
94
      end
95

96
      def meets_dependency?(params, request_params)
49✔
97
        return true unless @dependent_on
379,478✔
98
        return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
46,074✔
99

100
        if params.is_a?(Array)
45,990✔
101
          @params_meeting_dependency = params.flatten.filter { |param| meets_dependency?(param, request_params) }
8,526✔
102
          return @params_meeting_dependency.present?
2,688✔
103
        end
104

105
        meets_hash_dependency?(params)
43,302✔
106
      end
107

108
      def attr_meets_dependency?(params)
49✔
109
        return true unless @dependent_on
5,208✔
110
        return false if @parent.present? && !@parent.attr_meets_dependency?(params)
1,974✔
111

112
        meets_hash_dependency?(params)
1,848✔
113
      end
114

115
      def meets_hash_dependency?(params)
49✔
116
        # params might be anything what looks like a hash, so it must implement a `key?` method
117
        return false unless params.respond_to?(:key?)
45,150✔
118

119
        @dependent_on.each do |dependency|
45,108✔
120
          if dependency.is_a?(Hash)
45,318✔
121
            dependency_key = dependency.keys[0]
39,648✔
122
            proc = dependency.values[0]
39,648✔
123
            return false unless proc.call(params.try(:[], dependency_key))
39,648✔
124
          elsif params.respond_to?(:key?) && params.try(:[], dependency).blank?
5,670✔
125
            return false
1,764✔
126
          end
127
        end
128

129
        true
24,738✔
130
      end
131

132
      # @return [String] the proper attribute name, with nesting considered.
133
      def full_name(name, index: nil)
49✔
134
        if nested?
308,470✔
135
          # Find our containing element's name, and append ours.
136
          "#{@parent.full_name(@element)}#{brackets(index || @index)}#{brackets(name)}"
81,583✔
137
        elsif lateral?
226,887✔
138
          # Find the name of the element as if it was at the same nesting level
139
          # as our parent. We need to forward our index upward to achieve this.
140
          @parent.full_name(name, index: @index)
24,234✔
141
        else
142
          # We must be the root scope, so no prefix needed.
143
          name.to_s
202,653✔
144
        end
145
      end
146

147
      def brackets(val)
49✔
148
        "[#{val}]" if val
163,166✔
149
      end
150

151
      # @return [Boolean] whether or not this scope is the root-level scope
152
      def root?
49✔
153
        !@parent
126✔
154
      end
155

156
      # A nested scope is contained in one of its parent's elements.
157
      # @return [Boolean] whether or not this scope is nested
158
      def nested?
49✔
159
        @parent && @element
436,462✔
160
      end
161

162
      # A lateral scope is subordinate to its parent, but its keys are at the
163
      # same level as its parent and thus is not contained within an element.
164
      # @return [Boolean] whether or not this scope is lateral
165
      def lateral?
49✔
166
        @parent && !@element
409,463✔
167
      end
168

169
      # @return [Boolean] whether or not this scope needs to be present, or can
170
      #   be blank
171
      def required?
49✔
172
        !@optional
121,614✔
173
      end
174

175
      def reset_index
49✔
176
        @index = nil
129,805✔
177
      end
178

179
      protected
49✔
180

181
      # Adds a parameter declaration to our list of validations.
182
      # @param attrs [Array] (see Grape::DSL::Parameters#requires)
183
      def push_declared_params(attrs, opts = {})
49✔
184
        opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope)
167,456✔
185
        return @parent.push_declared_params(attrs, opts) if lateral?
167,456✔
186

187
        push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as]
151,958✔
188
        @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
306,898✔
189
      end
190

191
      # Get the full path of the parameter scope in the hierarchy.
192
      #
193
      # @return [Array<Symbol>] the nesting/path of the current parameter scope
194
      def full_path
49✔
195
        if nested?
9,408✔
196
          (@parent.full_path + [@element])
6,048✔
197
        elsif lateral?
3,360✔
198
          @parent.full_path
42✔
199
        else
200
          []
3,318✔
201
        end
202
      end
203

204
      private
49✔
205

206
      # Add a new parameter which should be renamed when using the +#declared+
207
      # method.
208
      #
209
      # @param path [Array<String, Symbol>] the full path of the parameter
210
      #   (including the parameter name as last array element)
211
      # @param new_name [String, Symbol] the new name of the parameter (the
212
      #   renamed name, with the +as: ...+ semantic)
213
      def push_renamed_param(path, new_name)
49✔
214
        api_route_setting = @api.inheritable_setting.route
3,318✔
215
        base = api_route_setting[:renamed_params] || {}
3,318✔
216
        base[Array(path).map(&:to_s)] = new_name.to_s
3,318✔
217
        api_route_setting[:renamed_params] = base
3,318✔
218
      end
219

220
      def require_required_and_optional_fields(context, opts)
49✔
221
        if context == :all
504✔
222
          optional_fields = Array.wrap(opts[:except])
294✔
223
          required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) }
966✔
224
        else # context == :none
225
          required_fields = Array.wrap(opts[:except])
210✔
226
          optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) }
630✔
227
        end
228
        required_fields.each do |field|
504✔
229
          field_opts = opts[:using][field]
546✔
230
          raise ArgumentError, "required field not exist: #{field}" unless field_opts
546✔
231

232
          requires(field, field_opts)
504✔
233
        end
234
        optional_fields.each do |field|
462✔
235
          field_opts = opts[:using][field]
546✔
236
          optional(field, field_opts) if field_opts
546✔
237
        end
238
      end
239

240
      def require_optional_fields(context, opts)
49✔
241
        optional_fields = opts[:using].keys
294✔
242
        unless context == :all
294!
243
          except_fields = Array.wrap(opts[:except])
×
UNCOV
244
          optional_fields.delete_if { |f| except_fields.include?(f) }
×
245
        end
246
        optional_fields.each do |field|
294✔
247
          field_opts = opts[:using][field]
504✔
248
          optional(field, field_opts) if field_opts
504!
249
        end
250
      end
251

252
      def validate_attributes(attrs, opts, &block)
49✔
253
        validations = opts.clone
152,756✔
254
        validations[:type] ||= Array if block
152,756✔
255
        validates(attrs, validations)
152,756✔
256
      end
257

258
      # Returns a new parameter scope, subordinate to the current one and nested
259
      # under the parameter corresponding to `attrs.first`.
260
      # @param attrs [Array] the attributes passed to the `requires` or
261
      #   `optional` invocation that opened this scope.
262
      # @param optional [Boolean] whether the parameter this are nested under
263
      #   is optional or not (and hence, whether this block's params will be).
264
      # @yield parameter scope
265
      def new_scope(attrs, optional = false, &block)
49✔
266
        # if required params are grouped and no type or unsupported type is provided, raise an error
267
        type = attrs[1] ? attrs[1][:type] : nil
21,803✔
268
        if attrs.first && !optional
21,803✔
269
          raise Grape::Exceptions::MissingGroupType if type.nil?
7,098✔
270
          raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
7,056✔
271
        end
272

273
        self.class.new(
21,719✔
274
          api: @api,
275
          element: attrs.first,
276
          element_renamed: attrs[1][:as],
277
          parent: self,
278
          optional: optional,
279
          type: type || Array,
280
          group: @group,
281
          &block
282
        )
283
      end
284

285
      # Returns a new parameter scope, not nested under any current-level param
286
      # but instead at the same level as the current scope.
287
      # @param options [Hash] options to control how this new scope behaves
288
      # @option options :dependent_on [Symbol] if given, specifies that this
289
      #   scope should only validate if this parameter from the above scope is
290
      #   present
291
      # @yield parameter scope
292
      def new_lateral_scope(options, &block)
49✔
293
        self.class.new(
10,500✔
294
          api: @api,
295
          element: nil,
296
          parent: self,
297
          options: @optional,
298
          type: type == Array ? Array : Hash,
10,500✔
299
          dependent_on: options[:dependent_on],
300
          &block
301
        )
302
      end
303

304
      # Returns a new parameter scope, subordinate to the current one and nested
305
      # under the parameter corresponding to `attrs.first`.
306
      # @param attrs [Array] the attributes passed to the `requires` or
307
      #   `optional` invocation that opened this scope.
308
      # @yield parameter scope
309
      def new_group_scope(attrs, &block)
49✔
310
        self.class.new(api: @api, parent: self, group: attrs.first, &block)
3,024✔
311
      end
312

313
      # Pushes declared params to parent or settings
314
      def configure_declared_params
49✔
315
        push_renamed_param(full_path, @element_renamed) if @element_renamed
118,584✔
316

317
        if nested?
118,584✔
318
          @parent.push_declared_params [element => @declared_params]
21,719✔
319
        else
320
          @api.namespace_stackable(:declared_params, @declared_params)
96,865✔
321
        end
322

323
        # params were stored in settings, it can be cleaned from the params scope
324
        @declared_params = nil
118,584✔
325
      end
326

327
      def validates(attrs, validations)
49✔
328
        coerce_type = infer_coercion(validations)
156,494✔
329
        required = validations.key?(:presence)
156,452✔
330
        default = validations[:default]
156,452✔
331
        values = validations[:values].is_a?(Hash) ? validations.dig(:values, :value) : validations[:values]
156,452✔
332
        except_values = validations[:except_values].is_a?(Hash) ? validations.dig(:except_values, :value) : validations[:except_values]
156,452✔
333

334
        # NB. values and excepts should be nil, Proc, Array, or Range.
335
        # Specifically, values should NOT be a Hash
336
        # use values or excepts to guess coerce type when stated type is Array
337
        coerce_type = guess_coerce_type(coerce_type, values, except_values)
156,452✔
338

339
        # default value should be present in values array, if both exist and are not procs
340
        check_incompatible_option_values(default, values, except_values)
156,452✔
341

342
        # type should be compatible with values array, if both exist
343
        validate_value_coercion(coerce_type, values, except_values)
156,284✔
344

345
        document_params attrs, validations, coerce_type, values, except_values
155,906✔
346

347
        opts = derive_validator_options(validations)
155,906✔
348

349
        # Validate for presence before any other validators
350
        validates_presence(validations, attrs, opts)
155,906✔
351

352
        # Before we run the rest of the validators, let's handle
353
        # whatever coercion so that we are working with correctly
354
        # type casted values
355
        coerce_type validations, attrs, required, opts
155,906✔
356

357
        validations.each do |type, options|
155,780✔
358
          # Don't try to look up validators for documentation params that don't have one.
359
          next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
92,947✔
360

361
          validate(type, options, attrs, required, opts)
88,789✔
362
        end
363
      end
364

365
      # Validate and comprehend the +:type+, +:types+, and +:coerce_with+
366
      # options that have been supplied to the parameter declaration.
367
      # The +:type+ and +:types+ options will be removed from the
368
      # validations list, replaced appropriately with +:coerce+ and
369
      # +:coerce_with+ options that will later be passed to
370
      # {Validators::CoerceValidator}. The type that is returned may be
371
      # used for documentation and further validation of parameter
372
      # options.
373
      #
374
      # @param validations [Hash] list of validations supplied to the
375
      #   parameter declaration
376
      # @return [class-like] type to which the parameter will be coerced
377
      # @raise [ArgumentError] if the given type options are invalid
378
      def infer_coercion(validations)
49✔
379
        raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
156,494✔
380

381
        validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
156,452✔
382
        validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
156,452✔
383
        validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
156,452✔
384
        validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
156,452✔
385

386
        validations.delete(:types) if validations.key?(:types)
156,452✔
387

388
        coerce_type = validations[:coerce]
156,452✔
389

390
        # Special case - when the argument is a single type that is a
391
        # variant-type collection.
392
        if Types.multiple?(coerce_type) && validations.key?(:type)
156,452✔
393
          validations[:coerce] = Types::VariantCollectionCoercer.new(
465✔
394
            coerce_type,
395
            validations.delete(:coerce_with)
396
          )
397
        end
398
        validations.delete(:type)
156,452✔
399

400
        coerce_type
156,452✔
401
      end
402

403
      # Enforce correct usage of :coerce_with parameter.
404
      # We do not allow coercion without a type, nor with
405
      # +JSON+ as a type since this defines its own coercion
406
      # method.
407
      def check_coerce_with(validations)
49✔
408
        return unless validations.key?(:coerce_with)
155,906✔
409
        # type must be supplied for coerce_with..
410
        raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
1,218✔
411

412
        # but not special JSON types, which
413
        # already imply coercion method
414
        return if [JSON, Array[JSON]].exclude? validations[:coerce]
1,176✔
415

416
        raise ArgumentError, 'coerce_with disallowed for type: JSON'
84✔
417
      end
418

419
      # Add type coercion validation to this scope,
420
      # if any has been specified.
421
      # This validation has special handling since it is
422
      # composited from more than one +requires+/+optional+
423
      # parameter, and needs to be run before most other
424
      # validations.
425
      def coerce_type(validations, attrs, required, opts)
49✔
426
        check_coerce_with(validations)
155,906✔
427

428
        return unless validations.key?(:coerce)
155,780✔
429

430
        coerce_options = {
431
          type: validations[:coerce],
76,475✔
432
          method: validations[:coerce_with],
433
          message: validations[:coerce_message]
434
        }
435
        validate('coerce', coerce_options, attrs, required, opts)
76,475✔
436
        validations.delete(:coerce_with)
76,475✔
437
        validations.delete(:coerce)
76,475✔
438
        validations.delete(:coerce_message)
76,475✔
439
      end
440

441
      def guess_coerce_type(coerce_type, *values_list)
49✔
442
        return coerce_type unless coerce_type == Array
156,452✔
443

444
        values_list.each do |values|
14,578✔
445
          next if !values || values.is_a?(Proc)
29,072✔
446
          return values.first.class if values.is_a?(Range) || !values.empty?
84!
447
        end
448
        coerce_type
14,494✔
449
      end
450

451
      def check_incompatible_option_values(default, values, except_values)
49✔
452
        return unless default && !default.is_a?(Proc)
156,452✔
453

454
        raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
24,613✔
455

456
        return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
17,053✔
457

458
        raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
84✔
459
      end
460

461
      def validate(type, options, attrs, required, opts)
49✔
462
        validator_options = {
463
          attributes: attrs,
234,235✔
464
          options: options,
465
          required: required,
466
          params_scope: self,
467
          opts: opts,
468
          validator_class: Validations.require_validator(type)
469
        }
470
        @api.namespace_stackable(:validations, validator_options)
234,235✔
471
      end
472

473
      def validate_value_coercion(coerce_type, *values_list)
49✔
474
        return unless coerce_type
156,284✔
475

476
        coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
76,895✔
477
        values_list.each do |values|
76,895✔
478
          next if !values || values.is_a?(Proc)
153,496✔
479

480
          value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
19,068✔
481
          value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
20,874✔
482
          raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
19,068✔
483
        end
484
      end
485

486
      def extract_message_option(attrs)
49✔
487
        return nil unless attrs.is_a?(Array)
3,738!
488

489
        opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
3,738✔
490
        opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
3,738✔
491
      end
492

493
      def options_key?(type, key, validations)
49✔
494
        validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
153,034✔
495
      end
496

497
      def all_element_blank?(scoped_params)
49✔
498
        scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
14,112✔
499
      end
500

501
      # Validators don't have access to each other and they don't need, however,
502
      # some validators might influence others, so their options should be shared
503
      def derive_validator_options(validations)
49✔
504
        allow_blank = validations[:allow_blank]
155,906✔
505

506
        {
507
          allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
311,812✔
508
          fail_fast: validations.delete(:fail_fast) || false
509
        }
510
      end
511

512
      def validates_presence(validations, attrs, opts)
49✔
513
        return unless validations.key?(:presence) && validations[:presence]
155,906✔
514

515
        validate('presence', validations.delete(:presence), attrs, true, opts)
68,971✔
516
        validations.delete(:message) if validations.key?(:message)
68,971✔
517
      end
518
    end
519
  end
520
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc