Skip to content

Commit 07fcf03

Browse files
authored
Merge pull request #2623 from ruby-grape/fix_coercer_cache
Refactor Coercer Caching to Use Grape::Util::Cache
2 parents 3676029 + 2e1103a commit 07fcf03

File tree

8 files changed

+67
-83
lines changed

8 files changed

+67
-83
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [#2612](https://github.com/ruby-grape/grape/pull/2612): Avoid multiple mount pollution - [@alexanderadam](https://github.com/alexanderadam).
2525
* [#2617](https://github.com/ruby-grape/grape/pull/2617): Migrate from `ActiveSupport::Configurable` to `Dry::Configurable` - [@ericproulx](https://github.com/ericproulx).
2626
* [#2618](https://github.com/ruby-grape/grape/pull/2618): Modernize argument delegation for Ruby 3+ compatibility - [@ericproulx](https://github.com/ericproulx).
27+
* [#2623](https://github.com/ruby-grape/grape/pull/2623): Refactor coercer caching to use `Grape::Util::Cache` - [@ericproulx](https://github.com/ericproulx).
2728
* Your contribution here.
2829

2930
#### Fixes

lib/grape/dry_types.rb

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,53 @@
22

33
module Grape
44
module DryTypes
5-
# Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
6-
# a container in this case. Check documentation for more information
7-
# https://dry-rb.org/gems/dry-types/1.2/getting-started/
8-
include Dry.Types()
5+
# https://dry-rb.org/gems/dry-types/main/getting-started/
6+
# limit to what Grape is using
7+
include Dry.Types(:params, :coercible, :strict)
8+
9+
class StrictCache < Grape::Util::Cache
10+
MAPPING = {
11+
Grape::API::Boolean => DryTypes::Strict::Bool,
12+
BigDecimal => DryTypes::Strict::Decimal,
13+
Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
14+
TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
15+
FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
16+
}.freeze
17+
18+
def initialize
19+
super
20+
@cache = Hash.new do |h, strict_type|
21+
h[strict_type] = MAPPING.fetch(strict_type) do
22+
DryTypes.wrapped_dry_types_const_get(DryTypes::Strict, strict_type)
23+
end
24+
end
25+
end
26+
end
27+
28+
class ParamsCache < Grape::Util::Cache
29+
MAPPING = {
30+
Grape::API::Boolean => DryTypes::Params::Bool,
31+
BigDecimal => DryTypes::Params::Decimal,
32+
Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
33+
TrueClass => DryTypes::Params::Bool.constrained(eql: true),
34+
FalseClass => DryTypes::Params::Bool.constrained(eql: false),
35+
String => DryTypes::Coercible::String
36+
}.freeze
37+
38+
def initialize
39+
super
40+
@cache = Hash.new do |h, params_type|
41+
h[params_type] = MAPPING.fetch(params_type) do
42+
DryTypes.wrapped_dry_types_const_get(DryTypes::Params, params_type)
43+
end
44+
end
45+
end
46+
end
47+
48+
def self.wrapped_dry_types_const_get(dry_type, type)
49+
dry_type.const_get(type.name, false)
50+
rescue NameError
51+
raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
52+
end
953
end
1054
end

lib/grape/validations/types.rb

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,10 @@ def map_special(type)
155155
# @return [Object] object to be used
156156
# for coercion and type validation
157157
def build_coercer(type, method: nil, strict: false)
158-
cache_instance(type, method, strict) do
159-
create_coercer_instance(type, method, strict)
160-
end
158+
# no cache since unique
159+
return create_coercer_instance(type, method, strict) if method.respond_to?(:call)
160+
161+
CoercerCache[[type, method, strict]]
161162
end
162163

163164
def create_coercer_instance(type, method, strict)
@@ -184,30 +185,14 @@ def create_coercer_instance(type, method, strict)
184185
end
185186
end
186187

187-
def cache_instance(type, method, strict, &_block)
188-
key = cache_key(type, method, strict)
189-
190-
return @__cache[key] if @__cache.key?(key)
191-
192-
instance = yield
193-
194-
@__cache_write_lock.synchronize do
195-
@__cache[key] = instance
188+
class CoercerCache < Grape::Util::Cache
189+
def initialize
190+
super
191+
@cache = Hash.new do |h, (type, method, strict)|
192+
h[[type, method, strict]] = Grape::Validations::Types.create_coercer_instance(type, method, strict)
193+
end
196194
end
197-
198-
instance
199195
end
200-
201-
def cache_key(type, method, strict)
202-
[type, method, strict].each_with_object(+'_') do |val, memo|
203-
next if val.nil?
204-
205-
memo << '_' << val.to_s
206-
end
207-
end
208-
209-
instance_variable_set(:@__cache, {})
210-
instance_variable_set(:@__cache_write_lock, Mutex.new)
211196
end
212197
end
213198
end

lib/grape/validations/types/array_coercer.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ module Types
77
# an array of arrays of integers.
88
#
99
# It could've been possible to use an +of+
10-
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
10+
# method (https://dry-rb.org/gems/dry-types/main/array-with-member/)
1111
# provided by dry-types. Unfortunately, it doesn't work for Grape because of
1212
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
1313
# maintains Virtus behavior in coercing.
1414
class ArrayCoercer < DryTypeCoercer
1515
def initialize(type, strict = false)
1616
super
17-
18-
@coercer = scope::Array
17+
@coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
1918
@subtype = type.first
2019
end
2120

lib/grape/validations/types/dry_type_coercer.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
# frozen_string_literal: true
22

3-
module DryTypes
4-
# Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
5-
# a container in this case. Check documentation for more information
6-
# https://dry-rb.org/gems/dry-types/1.2/getting-started/
7-
include Dry.Types()
8-
end
9-
103
module Grape
114
module Validations
125
module Types
136
# A base class for classes which must identify a coercer to be used.
147
# If the +strict+ argument is true, it won't coerce the given value
158
# but check its type. More information there
16-
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
9+
# https://dry-rb.org/gems/dry-types/main/built-in-types/
1710
class DryTypeCoercer
1811
class << self
1912
# Returns a collection coercer which corresponds to a given type.
@@ -42,7 +35,7 @@ def coercer_instance_for(type, strict = false)
4235
def initialize(type, strict = false)
4336
@type = type
4437
@strict = strict
45-
@scope = strict ? DryTypes::Strict : DryTypes::Params
38+
@cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
4639
end
4740

4841
# Coerces the given value to a type which was specified during
@@ -53,13 +46,13 @@ def call(val)
5346
return if val.nil?
5447

5548
@coercer[val]
56-
rescue Dry::Types::CoercionError => _e
49+
rescue Dry::Types::CoercionError
5750
InvalidValue.new
5851
end
5952

6053
protected
6154

62-
attr_reader :scope, :type, :strict
55+
attr_reader :type, :strict, :cache_coercer
6356
end
6457
end
6558
end

lib/grape/validations/types/primitive_coercer.rb

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,10 @@ module Types
77
# initialization. When +strict+ is true, it doesn't coerce a value but check
88
# that it has the proper type.
99
class PrimitiveCoercer < DryTypeCoercer
10-
MAPPING = {
11-
Grape::API::Boolean => DryTypes::Params::Bool,
12-
BigDecimal => DryTypes::Params::Decimal,
13-
Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
14-
TrueClass => DryTypes::Params::Bool.constrained(eql: true),
15-
FalseClass => DryTypes::Params::Bool.constrained(eql: false),
16-
17-
# unfortunately, a +Params+ scope doesn't contain String
18-
String => DryTypes::Coercible::String
19-
}.freeze
20-
21-
STRICT_MAPPING = {
22-
Grape::API::Boolean => DryTypes::Strict::Bool,
23-
BigDecimal => DryTypes::Strict::Decimal,
24-
Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
25-
TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
26-
FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
27-
}.freeze
28-
2910
def initialize(type, strict = false)
3011
super
3112

32-
@type = type
33-
34-
@coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
35-
scope.const_get(type.name, false)
36-
rescue NameError
37-
raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
38-
39-
type
40-
end
13+
@coercer = cache_coercer[type]
4114
end
4215

4316
def call(val)

spec/grape/validations/types/primitive_coercer_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
let(:type) { Complex }
115115

116116
it 'raises error on init' do
117-
expect(DryTypes::Params.constants).not_to include(type.name.to_sym)
117+
expect(Grape::DryTypes::Params.constants).not_to include(type.name.to_sym)
118118
expect { subject }.to raise_error(/type Complex should support coercion/)
119119
end
120120
end

spec/grape/validations/types_spec.rb

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,10 @@ def self.parse; end
9292
end
9393

9494
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-
10095
it 'caches the result of the build_coercer method' do
101-
original_cache = described_class.instance_variable_get(:@__cache)
102-
described_class.instance_variable_set(:@__cache, {})
103-
10496
a_coercer = described_class.build_coercer(Array[String])
10597
b_coercer = described_class.build_coercer(Array[String])
106-
10798
expect(a_coercer.object_id).to eq(b_coercer.object_id)
108-
109-
described_class.instance_variable_set(:@__cache, original_cache)
11099
end
111100
end
112101
end

0 commit comments

Comments
 (0)