Instantiate validators at definition time #2657
Draft
+2,162
−1,586
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Instantiate validators at compile time instead of runtime
Summary
Validators are now instantiated at compile time (in
ParamsScopeandContractScope) rather than at request time viaValidatorFactory. Validators are thread safe since their initial state always stays the same. This eliminates per-request object allocation overhead and allows expensive setup (message resolution, option parsing) to happen once.Changes
Core architecture
ParamsScope#validateandContractScopenow store validator instances instead of option hashes innamespace_stackable[:validations]ParamsScope#coerce_typereceivesvalidations.extract!(:coerce, :coerce_with, :coerce_message)instead of the full hash, replacing the individualdeletecalls that followedEndpoint#run_validatorstakes arequest:keyword arg, reads validators directly frominheritable_setting.route[:saved_validations], and short-circuits withreturn if validators.blank?Endpoint#validationsenumerator method entirelyValidatorFactory-- its indirection is no longer neededValidator base (
Validators::Base)fail_fast?is now an explicit public methodvalidate!,message,options_key?to privateoptions_key?simplified to a singlekeyparameter (removed unused optional second arg)hash_like?,option_value,scrubmessagenow accepts a block for lazy default message generation (default_key || yield)@fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)in constructorself.inheritedhook reorganized (moved in file, unchanged behavior)Validator optimizations -- eagerly compute in
initializeAllowBlankValidator: caches@value(viaoption_value) and@exception_message; useshash_like?andscrubhelpersCoerceValidator: resolves type and builds converter in constructor; caches@exception_message; inlinesvalid_type?check; removestypemethod,converterattr_reader, andvalid_type?method; useshash_like?instead ofparams.is_a?(Hash)DefaultValidator: pre-builds a@default_calllambda; removesvalidate_param!entirely, inlining the call intovalidate!ExceptValuesValidator: validates proc arity in constructor; pre-builds@excepts_calllambda viaoption_value; caches@exception_message; useshash_like?LengthValidator: validates arguments and builds@exception_messageusing block-basedmessage { build_message_exception }; renamesbuild_messageto privatebuild_message_exception; simplifies nil checksValuesValidator: validates proc arity in constructor; pre-builds@values_calllambda viaoption_value; caches@exception_message; useshash_like?andscrubhelpersRegexpValidator: caches@value(viaoption_value) and@exception_message; removes localscrubmethod (now usesBase#scrub); useshash_like?SameAsValidator: eagerly resolves@valueand@exception_message(withI18n.tformatting) in constructor; removesbuild_messagePresenceValidator: caches@exception_message; useshash_like?AllOrNoneOfValidator,MutuallyExclusiveValidator: cache@exception_messagein constructorAtLeastOneOfValidator: caches@exception_message; inverts guard toreturn if keys_in_common(params).any?ExactlyOneOfValidator: caches two messages:@exactly_one_exception_messageand@mutual_exclusion_exception_messageContractScopeValidator: no longer inherits fromBase; standalone class with constructor signature(schema:)and its ownfail_fast?methodparams:argument simplification across all validatorsGrape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)])simplified toparams: @scope.full_name(attr_name)throughout, sinceValidationnow coerces to array internallyValidation exception
Grape::Exceptions::Validation--attr_accessor :params, :message_keychanged toattr_reader;paramsis now always coerced to an array in the constructorSpecs
let_it_be(:app)with per-describelet(:app)blocks across all validator specs so each test group defines only the route(s) it needsendpoint_run_validators.grapenotification expectations for empty-validator cases (no longer instrumented when validators are blank)Removed
test-profdependencytest-profgem fromGemfilespec/config/spec_test_prof.rbconfigfrom the spec helper directory loader since no config files remainTest plan
bundle exec rspec)