Skip to content

Instantiate validators at definition time#2657

Draft
ericproulx wants to merge 4 commits intomasterfrom
revisit_validators
Draft

Instantiate validators at definition time#2657
ericproulx wants to merge 4 commits intomasterfrom
revisit_validators

Conversation

@ericproulx
Copy link
Contributor

@ericproulx ericproulx commented Feb 12, 2026

Instantiate validators at definition time and fix thread safety

Summary

Validators are now instantiated at definition time (in ParamsScope and ContractScope) rather than at request time via ValidatorFactory. This eliminates per-request object allocation overhead and allows expensive setup (option parsing, converter building) to happen once.

Because ParamsScope instances are now shared across all concurrent requests, the two instance variables previously mutated at request time (@index and @params_meeting_dependency) have been moved into a per-request ParamScopeTracker stored in Fiber[], eliminating a long-standing thread-safety race.

I18n translation is handled through a shared Grape::Util::Translation module. Validators that need interpolation parameters (e.g. LengthValidator, SameAsValidator) store a Hash { key: :length, min: 2, max: 5 } as their message. Exceptions::Base#translate_message dispatches on type — Symbol, Hash, Proc, or String — so translation with the correct locale always happens at error-raise time.

Changes

Core architecture

  • ParamsScope#validate and ContractScope now store validator instances instead of option hashes in namespace_stackable[:validations]
  • ParamsScope#coerce_type receives validations.extract!(:coerce, :coerce_with, :coerce_message) instead of the full hash
  • Endpoint#run_validators takes a request: keyword arg, reads validators directly from inheritable_setting.route[:saved_validations], and short-circuits with return if validators.blank?
  • Removed Endpoint#validations enumerator method
  • Removed ValidatorFactory

Thread safety (ParamScopeTracker)

  • @index and @params_meeting_dependency removed from ParamsScope
  • Per-request ParamScopeTracker stored in Fiber[FIBER_KEY], set/restored around each request in Endpoint#run_validators
  • AttributesIterator#store_indices writes current + ancestor indices; ParamsScope#full_name and #qualifying_params read from it

Shared translation module (Grape::Util::Translation)

  • Extracted translate (with fallback locale logic) into Grape::Util::Translation, included by both Exceptions::Base and Validators::Base
  • Exceptions::Base#translate_message now supports Hash messages: { key: :symbol, **interpolation_params } for deferred translation with interpolation

Validator base (Validators::Base)

  • fail_fast? is now an explicit public method
  • Moved validate!, message, options_key? to private
  • Added private helpers: hash_like?, option_value, scrub, translate_message
  • message accepts a block for lazy default generation

Validator optimizations — eagerly computed in initialize

  • AllowBlankValidator: caches @value and @exception_message
  • CoerceValidator: resolves type and builds converter at definition time; removes type, converter, valid_type?
  • DefaultValidator: pre-builds @default_call lambda
  • ExceptValuesValidator / ValuesValidator: validates proc arity; pre-builds call lambda
  • LengthValidator: stores a Hash message for deferred i18n translation with interpolation
  • RegexpValidator / SameAsValidator / PresenceValidator: cache exception messages
  • AllOrNoneOfValidator, AtLeastOneOfValidator, MutuallyExclusiveValidator, ExactlyOneOfValidator: cache exception messages
  • ContractScopeValidator: no longer inherits from Base; standalone class with (schema:) constructor

params: argument simplification

  • Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)]) simplified to params: @scope.full_name(attr_name) throughout, since Validation now coerces to array internally

Specs

  • Replaced shared let_it_be(:app) with per-describe let(:app) blocks
  • Added i18n tests for LengthValidator and SameAsValidator confirming request-time locale is used regardless of definition-time locale
  • Added zh-CN translations for length and same_as messages

Test plan

  • Run full test suite (bundle exec rspec)
  • Verify no regressions in validation behavior
  • Benchmark request throughput to confirm reduced per-request allocations

@ericproulx ericproulx marked this pull request as draft February 12, 2026 08:42
@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx force-pushed the revisit_validators branch 2 times, most recently from 5d145a5 to f87920f Compare February 12, 2026 08:49
@ericproulx
Copy link
Contributor Author

Missing UPGRADING notes. Working on it

@ericproulx ericproulx force-pushed the revisit_validators branch 3 times, most recently from 2ecb403 to cf04c9d Compare February 12, 2026 13:04
@dblock
Copy link
Member

dblock commented Feb 12, 2026

Is there a tradeoff with things like @exception_message = message(:all_or_none) being always allocated? can they become class variables?

@ericproulx
Copy link
Contributor Author

Is there a tradeoff with things like @exception_message = message(:all_or_none) being always allocated? can they become class variables?
message involves @option so it can't be a class variable.

@ericproulx ericproulx force-pushed the revisit_validators branch 12 times, most recently from e16efcf to 0b9e34b Compare February 18, 2026 22:27
@ericproulx ericproulx force-pushed the revisit_validators branch 8 times, most recently from a503556 to 2b16ba1 Compare February 23, 2026 11:05
@ericproulx ericproulx force-pushed the revisit_validators branch 7 times, most recently from b744723 to 30b09ea Compare February 28, 2026 17:03
@ericproulx ericproulx force-pushed the revisit_validators branch 4 times, most recently from 246bee6 to df31408 Compare March 9, 2026 22:51
ericproulx and others added 2 commits March 10, 2026 00:01
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `test-prof` gem from Gemfile
- Delete spec/config/spec_test_prof.rb (no-op BeforeAll adapter)
- Remove 'config' directory from spec_helper.rb auto-require loop
- Replace `let_it_be` with `let` in validator specs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the revisit_validators branch 2 times, most recently from 734f753 to f272db7 Compare March 9, 2026 23:16
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the revisit_validators branch from f272db7 to f17a1db Compare March 9, 2026 23:19
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the revisit_validators branch from f17a1db to 0824af6 Compare March 9, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants