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

ruby-grape / grape / 23097149382

14 Mar 2026 09:53PM UTC coverage: 96.715% (-0.1%) from 96.843%
23097149382

Pull #2657

github

ericproulx
Instantiate validators at definition time

Validators are now instantiated once at route definition time rather
than per-request, eliminating repeated allocation overhead. Instances
are frozen to make them safe for sharing across requests.

Freezing strategy: inputs (attrs, options, opts) are frozen at the DSL
boundary before entering the validator, so subclass ivars derived from
them are frozen by construction. Base.new reduces to super.freeze.
Remove freeze_state! and ValidatorFactory.

ParamsScope: precompute full_path via build_full_path before
instance_eval so child scopes can read the parent path immediately.
Simplify meets_hash_dependency? with all? and dependency.first.

Validators::Base: add validation_error! helper to replace repeated
Grape::Exceptions::Validation.new calls across single-attr validators.

Fix DefaultValidator to always dup duplicable default values regardless
of frozen state, preserving per-request isolation.

Add DeepFreeze utility (freezes Hash/Array/String recursively, skips
Procs, coercers, and classes). Add specs for DeepFreeze,
SameAsValidator, and ExceptValuesValidator.
Pull Request #2657: Instantiate validators at definition time

1073 of 1166 branches covered (92.02%)

Branch coverage included in aggregate %.

165 of 168 new or added lines in 19 files covered. (98.21%)

1 existing line in 1 file now uncovered.

3372 of 3430 relevant lines covered (98.31%)

32668.49 hits per line

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

91.55
/lib/grape/validations/validators/base.rb
1
# frozen_string_literal: true
2

3
module Grape
37✔
4
  module Validations
37✔
5
    module Validators
37✔
6
      # Base class for all parameter validators.
7
      #
8
      # == Freeze contract
9
      # Validator instances are shared across requests and are frozen after
10
      # initialization (via +.new+). All inputs (+options+, +opts+, +attrs+)
11
      # arrive pre-frozen from the DSL boundary, so subclass ivars derived
12
      # from them are frozen by construction. Lazy ivar assignment
13
      # (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
14
      class Base
37✔
15
        include Grape::Util::Translation
37✔
16

17
        attr_reader :attrs
37✔
18

19
        # Creates a new Validator from options specified
20
        # by a +requires+ or +optional+ directive during
21
        # parameter definition.
22
        # @param attrs [Array] names of attributes to which the Validator applies
23
        # @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
24
        # @param required [Boolean] attribute(s) are required or optional
25
        # @param scope [ParamsScope] parent scope for this Validator
26
        # @param opts [Hash] additional validation options
27
        def initialize(attrs, options, required, scope, opts)
37✔
28
          @attrs = Array(attrs).freeze
115,403✔
29
          @option = Grape::Util::DeepFreeze.deep_freeze(options)
115,403✔
30
          @required = required
115,403✔
31
          @scope = scope
115,403✔
32
          @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)
115,403✔
33
        end
34

35
        # Validates a given request.
36
        # @note Override #validate! unless you need to access the entire request.
37
        # @param request [Grape::Request] the request currently being handled
38
        # @raise [Grape::Exceptions::Validation] if validation failed
39
        # @return [void]
40
        def validate(request)
37✔
41
          return unless @scope.should_validate?(request.params)
112,843✔
42

43
          validate!(request.params)
93,251✔
44
        end
45

46
        def self.new(...)
37✔
47
          super.freeze
115,403✔
48
        end
49

50
        def self.inherited(klass)
37✔
51
          super
1,515✔
52
          Validations.register(klass)
1,515✔
53
        end
54

55
        def fail_fast?
37✔
56
          @fail_fast
14,049✔
57
        end
58

59
        # Validates a given parameter hash.
60
        # @note Override #validate_param! for per-parameter validation,
61
        #   or #validate if you need access to the entire request.
62
        # @param params [Hash] parameters to validate
63
        # @raise [Grape::Exceptions::Validation] if validation failed
64
        # @return [void]
65
        def validate!(params)
37✔
66
          attributes = SingleAttributeIterator.new(@attrs, @scope, params)
85,890✔
67
          # we collect errors inside array because
68
          # there may be more than one error per field
69
          array_errors = []
85,890✔
70

71
          attributes.each do |val, attr_name, empty_val|
85,890✔
72
            next if !@scope.required? && empty_val
93,634!
73
            next unless @scope.meets_dependency?(val, params)
93,634✔
74

75
            validate_param!(attr_name, val) if @required || (hash_like?(val) && val.key?(attr_name))
93,346✔
76
          rescue Grape::Exceptions::Validation => e
77
            array_errors << e
12,481✔
78
          end
79

80
          raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
85,890✔
81
        end
82

83
        protected
37✔
84

85
        # Validates a single attribute. Override in subclasses.
86
        # @param attr_name [Symbol, String] the attribute name
87
        # @param params [Hash] the parameter hash containing the attribute
88
        # @raise [Grape::Exceptions::Validation] if validation failed
89
        # @return [void]
90
        def validate_param!(attr_name, params)
37✔
NEW
UNCOV
91
          raise NotImplementedError
×
92
        end
93

94
        private
37✔
95

96
        def validation_error!(attr_name, message = @exception_message)
37✔
97
          raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: message)
9,728✔
98
        end
99

100
        def hash_like?(obj)
37✔
101
          obj.respond_to?(:key?)
280,922✔
102
        end
103

104
        def options_key?(key, options = nil)
37✔
105
          current_options = options || @option
126,058✔
106
          hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
126,058✔
107
        end
108

109
        # Returns the effective message for a validation error.
110
        # Prefers an explicit +:message+ option, then +default_key+.
111
        # If both are nil, the block (if given) is called to compute a fallback —
112
        # useful for validators that build a message Hash for deferred i18n interpolation.
113
        # @example
114
        #   @exception_message = message(:presence)             # symbol key or custom message
115
        #   @exception_message = message { build_hash_message } # computed fallback
116
        def message(default_key = nil)
37✔
117
          key = options_key?(:message) ? @option[:message] : default_key
109,226✔
118
          return key unless key.nil?
109,226!
119

NEW
120
          yield if block_given?
×
121
        end
122

123
        def option_value
37✔
124
          options_key?(:value) ? @option[:value] : @option
16,160✔
125
        end
126

127
        def scrub(value)
37✔
128
          return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding?
10,560✔
129

130
          value.scrub
96✔
131
        end
132
      end
133
    end
134
  end
135
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