Skip to content

Commit 251f0de

Browse files
committed
Create a new instance of a validator per validation. Cache for Virtus::Attribute.build calls.
1 parent 53aeee9 commit 251f0de

13 files changed

+155
-12
lines changed

.rubocop_todo.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2017-06-13 12:09:49 -0400 using RuboCop version 0.47.0.
3+
# on 2017-06-17 22:15:11 +0300 using RuboCop version 0.47.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -10,21 +10,21 @@
1010
Metrics/AbcSize:
1111
Max: 44
1212

13-
# Offense count: 278
13+
# Offense count: 279
1414
# Configuration parameters: CountComments, ExcludedMethods.
1515
Metrics/BlockLength:
1616
Max: 3117
1717

1818
# Offense count: 8
1919
# Configuration parameters: CountComments.
2020
Metrics/ClassLength:
21-
Max: 287
21+
Max: 288
2222

2323
# Offense count: 28
2424
Metrics/CyclomaticComplexity:
2525
Max: 14
2626

27-
# Offense count: 1108
27+
# Offense count: 1114
2828
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
2929
# URISchemes: http, https
3030
Metrics/LineLength:

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* [#1632](https://github.com/ruby-grape/grape/pull/1632): Silence warnings - [@thogg4](https://github.com/thogg4).
1616
* [#1615](https://github.com/ruby-grape/grape/pull/1615): Fix default and type validator when values is a Hash with no value attribute - [@jlfaber](https://github.com/jlfaber).
1717
* [#1625](https://github.com/ruby-grape/grape/pull/1625): Handle `given` correctly when nested in Array params - [@rnubel](https://github.com/rnubel), [@avellable](https://github.com/avellable).
18+
* [#1649](https://github.com/ruby-grape/grape/pull/1649): Don't share validator instances between requests - [@anakinj](https://github.com/anakinj).
1819
* Your contribution here.
1920

2021
### 0.19.2 (4/12/2017)

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,8 @@ params do
14221422
end
14231423
```
14241424

1425+
Every validation will have it's own instance of the validator, which means that the validator can have a state.
1426+
14251427
### Validation Errors
14261428

14271429
Validation and coercion errors are collected and an exception of type `Grape::Exceptions::ValidationErrors` is raised. If the exception goes uncaught it will respond with a status of 400 and an error message. The validation errors are grouped by parameter name and can be accessed via `Grape::Exceptions::ValidationErrors#errors`.

benchmark/simple_with_type_coercer.rb

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2+
require 'grape'
3+
require 'benchmark/ips'
4+
5+
api = Class.new(Grape::API) do
6+
prefix :api
7+
version 'v1', using: :path
8+
params do
9+
requires :param, type: Array[String]
10+
end
11+
get '/' do
12+
'hello'
13+
end
14+
end
15+
16+
env = Rack::MockRequest.env_for('/api/v1?param=value', method: 'GET')
17+
18+
Benchmark.ips do |ips|
19+
ips.report('simple_with_type_coercer') do
20+
api.call(env)
21+
end
22+
end

lib/grape.rb

+1
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,6 @@ module ServeFile
210210
require 'grape/validations/params_scope'
211211
require 'grape/validations/validators/all_or_none'
212212
require 'grape/validations/types'
213+
require 'grape/validations/validator_factory'
213214

214215
require 'grape/version'

lib/grape/endpoint.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,11 @@ def lazy_initialize!
338338
end
339339
end
340340

341-
def run_validators(validators, request)
341+
def run_validators(validator_factories, request)
342342
validation_errors = []
343343

344+
validators = validator_factories.map(&:create_validator)
345+
344346
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
345347
validators.each do |validator|
346348
begin

lib/grape/validations.rb

+4
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ class << self
1414
def self.register_validator(short_name, klass)
1515
validators[short_name] = klass
1616
end
17+
18+
def self.deregister_validator(short_name)
19+
validators.delete(short_name)
20+
end
1721
end
1822
end

lib/grape/validations/params_scope.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,13 @@ def validate(type, options, attrs, doc_attrs, opts)
398398

399399
raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
400400

401-
value = validator_class.new(attrs, options, doc_attrs[:required], self, opts)
402-
@api.namespace_stackable(:validations, value)
401+
factory = Grape::Validations::ValidatorFactory.new(attributes: attrs,
402+
options: options,
403+
required: doc_attrs[:required],
404+
params_scope: self,
405+
opts: opts,
406+
validator_class: validator_class)
407+
@api.namespace_stackable(:validations, factory)
403408
end
404409

405410
def validate_value_coercion(coerce_type, *values_list)

lib/grape/validations/types/build_coercer.rb

+27
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ module Types
2020
# @return [Virtus::Attribute] object to be used
2121
# for coercion and type validation
2222
def self.build_coercer(type, method = nil)
23+
cache_instance(type, method) do
24+
create_coercer_instance(type, method)
25+
end
26+
end
27+
28+
def self.create_coercer_instance(type, method = nil)
2329
# Accept pre-rolled virtus attributes without interference
2430
return type if type.is_a? Virtus::Attribute
2531

@@ -56,6 +62,27 @@ def self.build_coercer(type, method = nil)
5662
# for many common ruby types.
5763
Virtus::Attribute.build(conversion_type, converter_options)
5864
end
65+
66+
def self.cache_instance(type, method, &_block)
67+
key = cache_key(type, method)
68+
69+
return @__cache[key] if @__cache.key?(key)
70+
71+
instance = yield
72+
73+
@__cache_write_lock.synchronize do
74+
@__cache[key] = instance
75+
end
76+
77+
instance
78+
end
79+
80+
def self.cache_key(type, method)
81+
[type, method].compact.map(&:to_s).join('_')
82+
end
83+
84+
instance_variable_set(:@__cache, {})
85+
instance_variable_set(:@__cache_write_lock, Mutex.new)
5986
end
6087
end
6188
end
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Grape
2+
module Validations
3+
class ValidatorFactory
4+
def initialize(**options)
5+
@validator_class = options.delete(:validator_class)
6+
@options = options
7+
end
8+
9+
def create_validator
10+
@validator_class.new(@options[:attributes],
11+
@options[:options],
12+
@options[:required],
13+
@options[:params_scope],
14+
@options[:opts])
15+
end
16+
end
17+
end
18+
end

lib/grape/validations/validators/base.rb

+3-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def initialize(attrs, options, required, scope, opts = {})
2020
end
2121

2222
# Validates a given request.
23-
# @note This method must be thread-safe.
2423
# @note Override #validate! unless you need to access the entire request.
2524
# @param request [Grape::Request] the request currently being handled
2625
# @raise [Grape::Exceptions::Validation] if validation failed
@@ -31,8 +30,7 @@ def validate(request)
3130
end
3231

3332
# Validates a given parameter hash.
34-
# @note This method must be thread-safe.
35-
# @note Override #validate iff you need to access the entire request.
33+
# @note Override #validate if you need to access the entire request.
3634
# @param params [Hash] parameters to validate
3735
# @raise [Grape::Exceptions::Validation] if validation failed
3836
# @return [void]
@@ -65,8 +63,8 @@ def self.convert_to_short_name(klass)
6563
end
6664

6765
def self.inherited(klass)
68-
short_name = convert_to_short_name(klass)
69-
Validations.register_validator(short_name, klass)
66+
return unless klass.name.present?
67+
Validations.register_validator(convert_to_short_name(klass), klass)
7068
end
7169

7270
def message(default_key = nil)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'spec_helper'
2+
3+
describe 'Validator with instance variables' do
4+
let(:validator_type) do
5+
Class.new(Grape::Validations::Base) do
6+
def validate_param!(_attr_name, _params)
7+
if @instance_variable
8+
raise Grape::Exceptions::Validation, params: ['params'],
9+
message: 'This should never happen'
10+
end
11+
@instance_variable = true
12+
end
13+
end
14+
end
15+
16+
before do
17+
Grape::Validations.register_validator('instance_validator', validator_type)
18+
end
19+
20+
after do
21+
Grape::Validations.deregister_validator('instance_validator')
22+
end
23+
24+
let(:app) do
25+
Class.new(Grape::API) do
26+
params do
27+
optional :param_to_validate, instance_validator: true
28+
optional :another_param_to_validate, instance_validator: true
29+
end
30+
get do
31+
'noop'
32+
end
33+
end
34+
end
35+
36+
it 'passes validation every time' do
37+
expect(validator_type).to receive(:new).exactly(4).times.and_call_original
38+
39+
2.times do
40+
get '/', param_to_validate: 'value', another_param_to_validate: 'value'
41+
expect(last_response.status).to eq 200
42+
end
43+
end
44+
end

spec/grape/validations/types_spec.rb

+19
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,23 @@ class VirtusC
9090
expect(described_class.custom?(TypesSpec::BarType)).to be_falsy
9191
end
9292
end
93+
94+
describe '::build_coercer' do
95+
it 'has internal cache variables' do
96+
expect(described_class.instance_variable_get(:@__cache)).to be_a(Hash)
97+
expect(described_class.instance_variable_get(:@__cache_write_lock)).to be_a(Mutex)
98+
end
99+
100+
it 'caches the result of the Virtus::Attribute.build method' do
101+
original_cache = described_class.instance_variable_get(:@__cache)
102+
described_class.instance_variable_set(:@__cache, {})
103+
104+
coercer = 'TestCoercer'
105+
expect(Virtus::Attribute).to receive(:build).once.and_return(coercer)
106+
expect(described_class.build_coercer(Array[String])).to eq(coercer)
107+
expect(described_class.build_coercer(Array[String])).to eq(coercer)
108+
109+
described_class.instance_variable_set(:@__cache, original_cache)
110+
end
111+
end
93112
end

0 commit comments

Comments
 (0)