Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* [#2612](https://github.com/ruby-grape/grape/pull/2612): Avoid multiple mount pollution - [@alexanderadam](https://github.com/alexanderadam).
* [#2617](https://github.com/ruby-grape/grape/pull/2617): Migrate from `ActiveSupport::Configurable` to `Dry::Configurable` - [@ericproulx](https://github.com/ericproulx).
* [#2618](https://github.com/ruby-grape/grape/pull/2618): Modernize argument delegation for Ruby 3+ compatibility - [@ericproulx](https://github.com/ericproulx).
* [#2623](https://github.com/ruby-grape/grape/pull/2623): Refactor coercer caching to use `Grape::Util::Cache` - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
52 changes: 48 additions & 4 deletions lib/grape/dry_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,53 @@

module Grape
module DryTypes
# Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
# a container in this case. Check documentation for more information
# https://dry-rb.org/gems/dry-types/1.2/getting-started/
include Dry.Types()
# https://dry-rb.org/gems/dry-types/main/getting-started/
# limit to what Grape is using
include Dry.Types(:params, :coercible, :strict)

class StrictCache < Grape::Util::Cache
MAPPING = {
Grape::API::Boolean => DryTypes::Strict::Bool,
BigDecimal => DryTypes::Strict::Decimal,
Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
}.freeze

def initialize
super
@cache = Hash.new do |h, strict_type|
h[strict_type] = MAPPING.fetch(strict_type) do
DryTypes.wrapped_dry_types_const_get(DryTypes::Strict, strict_type)
end
end
end
end

class ParamsCache < Grape::Util::Cache
MAPPING = {
Grape::API::Boolean => DryTypes::Params::Bool,
BigDecimal => DryTypes::Params::Decimal,
Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
TrueClass => DryTypes::Params::Bool.constrained(eql: true),
FalseClass => DryTypes::Params::Bool.constrained(eql: false),
String => DryTypes::Coercible::String
}.freeze

def initialize
super
@cache = Hash.new do |h, params_type|
h[params_type] = MAPPING.fetch(params_type) do
DryTypes.wrapped_dry_types_const_get(DryTypes::Params, params_type)
end
end
end
end

def self.wrapped_dry_types_const_get(dry_type, type)
dry_type.const_get(type.name, false)
rescue NameError
raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
end
end
end
35 changes: 10 additions & 25 deletions lib/grape/validations/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ def map_special(type)
# @return [Object] object to be used
# for coercion and type validation
def build_coercer(type, method: nil, strict: false)
cache_instance(type, method, strict) do
create_coercer_instance(type, method, strict)
end
# no cache since unique
return create_coercer_instance(type, method, strict) if method.respond_to?(:call)

CoercerCache[[type, method, strict]]
end

def create_coercer_instance(type, method, strict)
Expand All @@ -184,30 +185,14 @@ def create_coercer_instance(type, method, strict)
end
end

def cache_instance(type, method, strict, &_block)
key = cache_key(type, method, strict)

return @__cache[key] if @__cache.key?(key)

instance = yield

@__cache_write_lock.synchronize do
@__cache[key] = instance
class CoercerCache < Grape::Util::Cache
def initialize
super
@cache = Hash.new do |h, (type, method, strict)|
h[[type, method, strict]] = Grape::Validations::Types.create_coercer_instance(type, method, strict)
end
end

instance
end

def cache_key(type, method, strict)
[type, method, strict].each_with_object(+'_') do |val, memo|
next if val.nil?

memo << '_' << val.to_s
end
end

instance_variable_set(:@__cache, {})
instance_variable_set(:@__cache_write_lock, Mutex.new)
end
end
end
5 changes: 2 additions & 3 deletions lib/grape/validations/types/array_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ module Types
# an array of arrays of integers.
#
# It could've been possible to use an +of+
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
# method (https://dry-rb.org/gems/dry-types/main/array-with-member/)
# provided by dry-types. Unfortunately, it doesn't work for Grape because of
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
# maintains Virtus behavior in coercing.
class ArrayCoercer < DryTypeCoercer
def initialize(type, strict = false)
super

@coercer = scope::Array
@coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
@subtype = type.first
end

Expand Down
15 changes: 4 additions & 11 deletions lib/grape/validations/types/dry_type_coercer.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
# frozen_string_literal: true

module DryTypes
# Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
# a container in this case. Check documentation for more information
# https://dry-rb.org/gems/dry-types/1.2/getting-started/
include Dry.Types()
end

module Grape
module Validations
module Types
# A base class for classes which must identify a coercer to be used.
# If the +strict+ argument is true, it won't coerce the given value
# but check its type. More information there
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
# https://dry-rb.org/gems/dry-types/main/built-in-types/
class DryTypeCoercer
class << self
# Returns a collection coercer which corresponds to a given type.
Expand Down Expand Up @@ -42,7 +35,7 @@ def coercer_instance_for(type, strict = false)
def initialize(type, strict = false)
@type = type
@strict = strict
@scope = strict ? DryTypes::Strict : DryTypes::Params
@cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
end

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

@coercer[val]
rescue Dry::Types::CoercionError => _e
rescue Dry::Types::CoercionError
InvalidValue.new
end

protected

attr_reader :scope, :type, :strict
attr_reader :type, :strict, :cache_coercer
end
end
end
Expand Down
29 changes: 1 addition & 28 deletions lib/grape/validations/types/primitive_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,10 @@ module Types
# initialization. When +strict+ is true, it doesn't coerce a value but check
# that it has the proper type.
class PrimitiveCoercer < DryTypeCoercer
MAPPING = {
Grape::API::Boolean => DryTypes::Params::Bool,
BigDecimal => DryTypes::Params::Decimal,
Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
TrueClass => DryTypes::Params::Bool.constrained(eql: true),
FalseClass => DryTypes::Params::Bool.constrained(eql: false),

# unfortunately, a +Params+ scope doesn't contain String
String => DryTypes::Coercible::String
}.freeze

STRICT_MAPPING = {
Grape::API::Boolean => DryTypes::Strict::Bool,
BigDecimal => DryTypes::Strict::Decimal,
Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
}.freeze

def initialize(type, strict = false)
super

@type = type

@coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
scope.const_get(type.name, false)
rescue NameError
raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])

type
end
@coercer = cache_coercer[type]
end

def call(val)
Expand Down
2 changes: 1 addition & 1 deletion spec/grape/validations/types/primitive_coercer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
let(:type) { Complex }

it 'raises error on init' do
expect(DryTypes::Params.constants).not_to include(type.name.to_sym)
expect(Grape::DryTypes::Params.constants).not_to include(type.name.to_sym)
expect { subject }.to raise_error(/type Complex should support coercion/)
end
end
Expand Down
11 changes: 0 additions & 11 deletions spec/grape/validations/types_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,10 @@ def self.parse; end
end

describe '::build_coercer' do
it 'has internal cache variables' do
expect(described_class.instance_variable_get(:@__cache)).to be_a(Hash)
expect(described_class.instance_variable_get(:@__cache_write_lock)).to be_a(Mutex)
end

it 'caches the result of the build_coercer method' do
original_cache = described_class.instance_variable_get(:@__cache)
described_class.instance_variable_set(:@__cache, {})

a_coercer = described_class.build_coercer(Array[String])
b_coercer = described_class.build_coercer(Array[String])

expect(a_coercer.object_id).to eq(b_coercer.object_id)

described_class.instance_variable_set(:@__cache, original_cache)
end
end
end