• 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

92.98
/lib/grape/validations/attributes_iterator.rb
1
# frozen_string_literal: true
2

3
module Grape
37✔
4
  module Validations
37✔
5
    class AttributesIterator
37✔
6
      include Enumerable
37✔
7

8
      attr_reader :scope
37✔
9

10
      def initialize(attrs, scope, params)
37✔
11
        @attrs = attrs
93,571✔
12
        @scope = scope
93,571✔
13
        @original_params = scope.params(params)
93,571✔
14
        @params = Array.wrap(@original_params)
93,571✔
15
      end
16

17
      def each(&)
37✔
18
        do_each(@params, &) # because we need recursion for nested arrays
93,571✔
19
      end
20

21
      private
37✔
22

23
      def do_each(params_to_process, parent_indicies = [], &block)
37✔
24
        params_to_process.each_with_index do |resource_params, index|
100,003✔
25
          # when we get arrays of arrays it means that target element located inside array
26
          # we need this because we want to know parent arrays indicies
27
          if resource_params.is_a?(Array)
112,963✔
28
            do_each(resource_params, [index] + parent_indicies, &block)
6,432✔
29
            next
6,432✔
30
          end
31

32
          if @scope.type == Array
106,531✔
33
            next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array
22,272✔
34

35
            store_indices(@scope, index, parent_indicies)
21,280✔
36
          elsif @original_params.is_a?(Array)
84,259✔
37
            # Non-Array-typed scope whose params derive from a parent Array scope.
38
            # Walk up the parent chain to find the nearest Array-typed ancestor
39
            # and record the index there so that full_name can produce correct bracket notation.
40
            target = nearest_array_ancestor
4,448✔
41
            store_indices(target, index, parent_indicies) if target
4,448✔
42
          end
43

44
          yield_attributes(resource_params, &block)
105,539✔
45
        end
46
      end
47

48
      def nearest_array_ancestor
37✔
49
        @nearest_array_ancestor ||= begin
4,448✔
50
          scope = @scope.parent
3,872✔
51
          scope = scope.parent while scope && scope.type != Array
3,872✔
52
          scope
3,872✔
53
        end
54
      end
55

56
      def store_indices(target_scope, index, parent_indicies)
37✔
57
        parent_scope = target_scope.parent
22,720✔
58
        parent_indicies.each do |parent_index|
22,720✔
59
          ScopeTracker.current&.store_index(parent_scope, parent_index)
10,336!
60
          parent_scope = parent_scope&.parent
10,336!
61
        end
62
        ScopeTracker.current&.store_index(target_scope, index)
22,720!
63
      end
64

65
      def yield_attributes(_resource_params)
37✔
UNCOV
66
        raise NotImplementedError
×
67
      end
68

69
      # This is a special case so that we can ignore tree's where option
70
      # values are missing lower down. Unfortunately we can remove this
71
      # are the parameter parsing stage as they are required to ensure
72
      # the correct indexing is maintained
73
      def skip?(val)
37✔
74
        val == Grape::DSL::Parameters::EmptyOptionalValue
105,539✔
75
      end
76
    end
77
  end
78
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