diff --git a/Gemfile.lock b/Gemfile.lock index fc5bb19..7d61571 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: memery (1.3.0) + ruby2_keywords (~> 0.0.2) GEM remote: https://rubygems.org/ @@ -65,6 +66,7 @@ GEM rubocop-rspec (1.37.1) rubocop (>= 0.68.1) ruby-progressbar (1.10.1) + ruby2_keywords (0.0.2) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) diff --git a/lib/memery.rb b/lib/memery.rb index 9be58ea..3d0c64a 100644 --- a/lib/memery.rb +++ b/lib/memery.rb @@ -1,20 +1,11 @@ # frozen_string_literal: true +require "ruby2_keywords" + require "memery/version" module Memery class << self - def method_visibility(klass, method_name) - case - when klass.private_method_defined?(method_name) - :private - when klass.protected_method_defined?(method_name) - :protected - when klass.public_method_defined?(method_name) - :public - end - end - def monotonic_clock Process.clock_gettime(Process::CLOCK_MONOTONIC) end @@ -48,39 +39,61 @@ def memoized?(method_name) def prepend_memery_module! return if defined?(@_memery_module) - @_memery_module = Module.new + @_memery_module = Module.new do + extend MemoizationModule + end prepend @_memery_module end - def define_memoized_method!(method_name, condition: nil, ttl: nil) - visibility = Memery.method_visibility(self, method_name) - raise ArgumentError, "Method #{method_name} is not defined on #{self}" unless visibility + def define_memoized_method!(*args, **kwargs) + @_memery_module.public_send __method__, self, *args, **kwargs + end - method_key = "#{method_name}_#{@_memery_module.object_id}" + module MemoizationModule + def define_memoized_method!(klass, method_name, condition: nil, ttl: nil) + method_key = "#{method_name}_#{object_id}" - # Change to regular call of `define_method` after Ruby 2.4 drop - @_memery_module.send :define_method, method_name, (lambda do |*args, **kwargs, &block| - if block || (condition && !instance_exec(&condition)) - return kwargs.any? ? super(*args, **kwargs, &block) : super(*args, &block) - end + original_visibility = method_visibility(klass, method_name) - args_key = [args, kwargs] + define_method method_name do |*args, &block| + if block || (condition && !instance_exec(&condition)) + return super(*args, &block) + end - store = (@_memery_memoized_values ||= {})[method_key] ||= {} + store = (@_memery_memoized_values ||= {})[method_key] ||= {} - if store.key?(args_key) && - (ttl.nil? || Memery.monotonic_clock <= store[args_key][:time] + ttl) - return store[args_key][:result] + if store.key?(args) && + (ttl.nil? || Memery.monotonic_clock <= store[args][:time] + ttl) + return store[args][:result] + end + + result = super(*args) + @_memery_memoized_values[method_key][args] = + { result: result, time: Memery.monotonic_clock } + result end - result = kwargs.any? ? super(*args, **kwargs) : super(*args) - @_memery_memoized_values[method_key][args_key] = - { result: result, time: Memery.monotonic_clock } - result - end) + ruby2_keywords method_name + + send original_visibility, method_name + end - @_memery_module.send(visibility, method_name) + private + + def method_visibility(klass, method_name) + if klass.private_method_defined?(method_name) + :private + elsif klass.protected_method_defined?(method_name) + :protected + elsif klass.public_method_defined?(method_name) + :public + else + raise ArgumentError, "Method #{method_name} is not defined on #{klass}" + end + end end + + private_constant :MemoizationModule end module InstanceMethods diff --git a/memery.gemspec b/memery.gemspec index 5071d33..19d8ce5 100644 --- a/memery.gemspec +++ b/memery.gemspec @@ -20,6 +20,8 @@ Gem::Specification.new do |spec| end spec.require_paths = ["lib"] + spec.add_runtime_dependency "ruby2_keywords", "~> 0.0.2" + spec.add_development_dependency "benchmark-ips" spec.add_development_dependency "benchmark-memory" spec.add_development_dependency "bundler" diff --git a/spec/memery_spec.rb b/spec/memery_spec.rb index 0aec975..2308c54 100644 --- a/spec/memery_spec.rb +++ b/spec/memery_spec.rb @@ -164,6 +164,24 @@ def self.macro(name) expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2]]) end + + context "receiving Hash-like object" do + let(:object_class) do + Struct.new(:first_name, :last_name) do + # For example, Sequel models have such implicit coercion, + # which conflicts with `**kwargs`. + alias_method :to_hash, :to_h + end + end + + let(:object) { object_class.new("John", "Wick") } + + specify do + values = [ a.m_args(1, object), a.m_args(1, object), a.m_args(1, 2) ] + expect(values).to eq([[1, object], [1, object], [1, 2]]) + expect(CALLS).to eq([[1, object], [1, 2]]) + end + end end context "method with keyword args" do