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

ruby-grape / grape / 22259784049

21 Feb 2026 03:58PM UTC coverage: 96.757% (-0.1%) from 96.906%
22259784049

Pull #2657

github

ericproulx
Instantiate validators at definition time and fix thread safety

Validator instantiation at definition time:
- Store validator instances in ParamsScope/ContractScope and have
  Endpoint#run_validators read them directly
- Remove ValidatorFactory indirection and eagerly compute validator
  messages/options in constructors
- Freeze validator instances after initialization via Base.new to
  prevent mutation across shared requests (shallow freeze)
- Extract Grape::Util::Translation module shared by Exceptions::Base
  and Validators::Base for I18n translate with fallback locale
- Support Hash messages in translate_message for deferred translation
  with interpolation parameters (e.g. { key: :length, min: 2 })
- Normalize Grape::Exceptions::Validation params handling and refactor
  validator specs to define routes per example group
- Use case/when for message_key extraction in Exceptions::Validation
- Guard LengthValidator against missing constraints and extract option
  validation into private methods to stay within complexity limits
- Store zero-arity procs directly in ValuesValidator (consistent with
  ExceptValuesValidator) and document DB-backed lazy evaluation intent
- Drop test-prof dependency and its spec config

Thread safety for shared ParamsScope instances:
- Introduce Grape::Validations::ScopeTracker to hold all per-request
  mutable state (array index and qualifying params) in a single
  Thread.current entry, keeping shared ParamsScope objects immutable
- ScopeTracker.track { } wraps the validation run in Endpoint and
  ensures cleanup via ensure regardless of errors
- AttributesIterator stores current array indices via ScopeTracker
  instead of mutating @index on the shared scope
- ParamsScope#full_name reads the current index from ScopeTracker
  instead of @index; remove @index, reset_index, and attr_accessor
- meets_dependency? stores qualifying array params in ScopeTracker
  instead of @params_meeting_dependency on the shared scope
- Change... (continued)
Pull Request #2657: Instantiate validators at definition time

1080 of 1172 branches covered (92.15%)

Branch coverage included in aggregate %.

269 of 271 new or added lines in 29 files covered. (99.26%)

1 existing line in 1 file now uncovered.

3366 of 3423 relevant lines covered (98.33%)

38934.63 hits per line

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

95.65
/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
      class Base
37✔
7
        include Grape::Util::Translation
37✔
8

9
        # Creates a new Validator from options specified
10
        # by a +requires+ or +optional+ directive during
11
        # parameter definition.
12
        # @param attrs [Array] names of attributes to which the Validator applies
13
        # @param options [Object] implementation-dependent Validator options
14
        # @param required [Boolean] attribute(s) are required or optional
15
        # @param scope [ParamsScope] parent scope for this Validator
16
        # @param opts [Hash] additional validation options
17
        def initialize(attrs, options, required, scope, opts)
37✔
18
          @attrs = Array(attrs)
160,811✔
19
          @option = options
160,811✔
20
          @required = required
160,811✔
21
          @scope = scope
160,811✔
22
          @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)
160,811✔
23
        end
24

25
        # Validates a given request.
26
        # @note Override #validate! unless you need to access the entire request.
27
        # @param request [Grape::Request] the request currently being handled
28
        # @raise [Grape::Exceptions::Validation] if validation failed
29
        # @return [void]
30
        def validate(request)
37✔
31
          return unless @scope.should_validate?(request.params)
112,875✔
32

33
          validate!(request.params)
93,283✔
34
        end
35

36
        def self.new(...)
37✔
37
          super.tap do |instance|
160,811✔
38
            instance.instance_variables.each do |ivar|
160,523✔
39
              Grape::Util::DeepFreeze.deep_freeze(instance.instance_variable_get(ivar))
1,221,296✔
40
            end
41
          end.freeze
42
        end
43

44
        def self.inherited(klass)
37✔
45
          super
1,515✔
46
          Validations.register(klass)
1,515✔
47
        end
48

49
        def fail_fast?
37✔
50
          @fail_fast
14,017✔
51
        end
52

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

65
          attributes.each do |val, attr_name, empty_val|
85,922✔
66
            next if !@scope.required? && empty_val
93,602!
67
            next unless @scope.meets_dependency?(val, params)
93,602✔
68

69
            validate_param!(attr_name, val) if @required || (hash_like?(val) && val.key?(attr_name))
93,314✔
70
          rescue Grape::Exceptions::Validation => e
71
            array_errors << e
12,417✔
72
          end
73

74
          raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
85,922✔
75
        end
76

77
        # Validates a single attribute.
78
        # @param attr_name [Symbol, String] the attribute name
79
        # @param params [Hash] the parameter hash containing the attribute
80
        # @raise [Grape::Exceptions::Validation] if validation failed
81
        # @return [void]
82
        def validate_param!(attr_name, params)
37✔
NEW
83
          raise NotImplementedError
×
84
        end
85

86
        private
37✔
87

88
        def hash_like?(obj)
37✔
89
          obj.respond_to?(:key?)
356,058✔
90
        end
91

92
        def options_key?(key, options = nil)
37✔
93
          current_options = options || @option
186,826✔
94
          hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
186,826✔
95
        end
96

97
        # Returns the effective message for a validation error.
98
        # Prefers an explicit +:message+ option, then +default_key+.
99
        # If both are nil, the block (if given) is called to compute a fallback —
100
        # useful for validators that build a message Hash for deferred i18n interpolation.
101
        # @example
102
        #   @exception_message = message(:presence)             # symbol key or custom message
103
        #   @exception_message = message { build_hash_message } # computed fallback
104
        def message(default_key = nil)
37✔
105
          key = options_key?(:message) ? @option[:message] : default_key
155,498✔
106
          return key unless key.nil?
155,498✔
107

108
          yield if block_given?
800!
109
        end
110

111
        def option_value
37✔
112
          options_key?(:value) ? @option[:value] : @option
31,328✔
113
        end
114

115
        def scrub(value)
37✔
116
          return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding?
10,496✔
117

118
          value.scrub
96✔
119
        end
120
      end
121
    end
122
  end
123
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