forget-me-not 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +16 -0
- data/lib/forget-me-not/cacheable.rb +140 -0
- data/lib/forget-me-not/hash_cache.rb +12 -0
- data/lib/forget-me-not/memoizable.rb +74 -0
- data/lib/forget-me-not/version.rb +3 -0
- data/lib/forget-me-not.rb +5 -0
- data/spec/cacheable_spec.rb +198 -0
- data/spec/memoizable_spec.rb +132 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/method_counter.rb +24 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YWI4NDI2YmQ5NTc1NjMyOGM5Y2UyZjk1NmFhYTcwODQyNWYzMzQ0Mw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NTFmOTRhYTg3NjNjZjAwM2JmNzIyYmRkNmI3YzM5NjJlNWY1ZjI4NA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MWJiNjdmODBmOTIwZGEzOThjNThjZDU2YjJlODk0M2RhN2Q2MjJkNjU5MjVj
|
10
|
+
YTZmYmU2ZDViYjg4MTc1Yzc0YjQ4M2ZmMjBjOTkxYzViZTAwZmQ4NzUzZTgw
|
11
|
+
OWQzMzRkODE0ZWMwN2VlNjIwNjk4NGJlYzAxMDY4NjE4ZjhhNjk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
M2MwOWE5MjFlN2U4ODRlN2VjMDg0MWQzYWZlODk0YmE2YmY5MmU5MjlhNmJj
|
14
|
+
MmJhNzk1N2RiMzM4YjMxNzY3MTlhYWI3MTYyM2FhYjViZTA4MTU5MDI4OWU0
|
15
|
+
ZGRiNWIxOTJkZWI5MmQxY2FjYzU3MzUwMTE3NDg4OGViM2NlYzA=
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## 0.0.1.0 - Pre Release
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Andy Davis
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# ForgetMeNot
|
2
|
+
[](http://travis-ci.org/KoanHealth/forget-me-not)
|
3
|
+
[](https://codeclimate.com/github/KoanHealth/forget-me-not)
|
4
|
+
[](https://coveralls.io/r/KoanHealth/forget-me-not)
|
5
|
+
|
6
|
+
Provides memoization and caching mixins
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'forget-me-not'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install forget-me-not
|
21
|
+
|
22
|
+
## Background
|
23
|
+
|
24
|
+
Although the code is quite similar between the two mixins they solve very different problems.
|
25
|
+
|
26
|
+
### Memoization
|
27
|
+
Memoization is intended to allow a single object instance to remember the result of a method call. If you have
|
28
|
+
ever written something like:
|
29
|
+
|
30
|
+
def full_name
|
31
|
+
@full_name ||= "#{last_name}, #{first_name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
then you have done memoization. The result is calculated the first time full_name is called. Subsequent calls return
|
35
|
+
the same value and do not incur the overhead of calculating the result. Trivial in this case, but the work could be quite
|
36
|
+
substantial.
|
37
|
+
|
38
|
+
Memoization is scoped to an object. If you ask two different User instances what the memoized full_name is, then
|
39
|
+
you will get a different answer from each of them.
|
40
|
+
|
41
|
+
### Caching
|
42
|
+
|
43
|
+
Caching is similar, but broader in scope. For us, the initial use case was a dashboard in one of our products that
|
44
|
+
aggregated the numerical values of several hundred thousand medical claims. Rather than performing this query every
|
45
|
+
time we generated the dashboard, we cached the results of the query allowing us to display the web page in a snap.
|
46
|
+
|
47
|
+
Our caching mixin is intended to be used with a cache like memcached that is available across your servers. It includes
|
48
|
+
convenience methods to allow cache warming.
|
49
|
+
|
50
|
+
Caching is system wide. If the same cached method for two separate instances is called, the actual method should only be
|
51
|
+
called once.
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
### Memoizable Mixin
|
56
|
+
class MyClass
|
57
|
+
include ForgetMeNot::Memoizable
|
58
|
+
|
59
|
+
def some_method
|
60
|
+
'result'
|
61
|
+
end
|
62
|
+
|
63
|
+
def some_other_method(with_an_arg)
|
64
|
+
"result2-#{with_an_arg}"
|
65
|
+
end
|
66
|
+
|
67
|
+
memoize :some_method, :some_other_method
|
68
|
+
end
|
69
|
+
|
70
|
+
Calls to both some_method and some_other_method are memoized. Notice that some_other_method takes an argument, differing
|
71
|
+
argument values will result in different results being memoized.
|
72
|
+
|
73
|
+
By default, the memoization code stores results in a simple Hash based cache. If you have other requirements, perhaps
|
74
|
+
a thread-safe storage, then set the ForgetMeNot::Memoization.storage_builder property to a proc that will create a new
|
75
|
+
instance of whatever storage you desire.
|
76
|
+
|
77
|
+
### Cacheable Mixin
|
78
|
+
The basics are unsurprising:
|
79
|
+
|
80
|
+
class MyClass
|
81
|
+
include ForgetMeNot::Cacheable
|
82
|
+
|
83
|
+
def some_method
|
84
|
+
'result'
|
85
|
+
end
|
86
|
+
|
87
|
+
cache :some_method
|
88
|
+
end
|
89
|
+
|
90
|
+
Like memoization, arguments are fully supported and will result in distinct storage to the cache.
|
91
|
+
|
92
|
+
To control warming the cache, implement the cache_warm method
|
93
|
+
|
94
|
+
class MyClass
|
95
|
+
include ForgetMeNot::Cacheable
|
96
|
+
|
97
|
+
def some_method
|
98
|
+
'result'
|
99
|
+
end
|
100
|
+
|
101
|
+
cache :some_method
|
102
|
+
|
103
|
+
# Warm the cache for this object
|
104
|
+
def self.cache_warm(*args)
|
105
|
+
instance = new
|
106
|
+
instance.some_method
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
Then somewhere (a rake task perhaps), call Cacheable.warm. Whatever args you pass to warm are passed to every class that
|
111
|
+
included Cacheable.
|
112
|
+
|
113
|
+
In addition to the arguments passed to the cached method, Cacheable allows instance properties to also be used as cache
|
114
|
+
key members.
|
115
|
+
|
116
|
+
class MyClass
|
117
|
+
include ForgetMeNot::Cacheable
|
118
|
+
|
119
|
+
attr_reader :important_property
|
120
|
+
def initialize(important_property)
|
121
|
+
@important_property = important_property
|
122
|
+
end
|
123
|
+
|
124
|
+
def some_method
|
125
|
+
'result'
|
126
|
+
end
|
127
|
+
|
128
|
+
cache :some_method, :include => :important_property
|
129
|
+
end
|
130
|
+
|
131
|
+
By default, the cache will attempt to use the Rails cache. If that isn't found, but ActiveSupport is available, a new
|
132
|
+
instance of MemoryStore will be used. Failing that, cache will raise an error. This is intended to provide a reasonably
|
133
|
+
sane default, but really, set the ForgetMeNot::Cacheable.cache. Cacheable expects a cache shaped like an
|
134
|
+
ActiveSupport::Cacheable::Store
|
135
|
+
|
136
|
+
|
137
|
+
## Origins
|
138
|
+
This is an extension of the ideas and approach found here:
|
139
|
+
https://github.com/sferik/twitter/blob/master/lib/twitter/memoizable.rb. You guys rock.
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
1. Fork it
|
144
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
145
|
+
3. Write tests for your code.
|
146
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
147
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
148
|
+
6. Create new Pull Request
|
149
|
+
|
150
|
+
## Copyright
|
151
|
+
(c) 2013 Koan Health. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new('spec') do |spec|
|
8
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new('spec:progress') do |spec|
|
12
|
+
spec.rspec_opts = %w(--format progress)
|
13
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => :spec
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module ForgetMeNot
|
2
|
+
# Allows the cross-system caching of lengthy function calls
|
3
|
+
module Cacheable
|
4
|
+
class << self
|
5
|
+
def included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
Cacheable.cachers << base
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def cache_results(*methods)
|
13
|
+
options = methods.last.is_a?(Hash) ? methods.pop : {}
|
14
|
+
methods.each { |m| cache_method(m, options) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache_warm(*args)
|
18
|
+
# Classes can call the methods necessary to warm the cache
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def cache_method(method_name, options)
|
23
|
+
method = instance_method(method_name)
|
24
|
+
visibility = method_visibility(method_name)
|
25
|
+
define_cache_method(method, options)
|
26
|
+
send(visibility, method_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_cache_method(method, options)
|
30
|
+
method_name = method.name.to_sym
|
31
|
+
key_prefix = "/cached_method_result/#{self.name}"
|
32
|
+
instance_key = get_instance_key_proc(options[:include]) if options.has_key?(:include)
|
33
|
+
|
34
|
+
undef_method(method_name)
|
35
|
+
define_method(method_name) do |*args|
|
36
|
+
cache_key = [
|
37
|
+
key_prefix,
|
38
|
+
(instance_key && instance_key.call(self)),
|
39
|
+
method_name,
|
40
|
+
args.hash
|
41
|
+
].compact.join '/'
|
42
|
+
|
43
|
+
puts "key: #{cache_key}" if (defined?(Rails) && Rails.env.test?)
|
44
|
+
|
45
|
+
Cacheable.cache_fetch(cache_key) do
|
46
|
+
method.bind(self).call(*args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_instance_key_proc(instance_key_methods)
|
52
|
+
instance_keys = Array.new(instance_key_methods).flatten
|
53
|
+
Proc.new do |instance|
|
54
|
+
instance_keys.map { |key| instance.send(key) }.hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_visibility(method)
|
59
|
+
if private_method_defined?(method)
|
60
|
+
:private
|
61
|
+
elsif protected_method_defined?(method)
|
62
|
+
:protected
|
63
|
+
else
|
64
|
+
:public
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.cache_fetch(key, &block)
|
70
|
+
cache.fetch(key, cache_options, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.cache
|
74
|
+
@cache ||= default_cache
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.cache=(value)
|
78
|
+
@cache = value
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.cache_options
|
82
|
+
@cache_options ||= {expires_in: 12 * 60 * 60}
|
83
|
+
@cache_options.merge(Cacheable.cache_options_threaded)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.cache_options_threaded
|
87
|
+
Thread.current['cacheable-cache-options'] || {}
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.cache_options_threaded=(options)
|
91
|
+
Thread.current['cacheable-cache-options'] = options
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.cachers
|
95
|
+
@cachers ||= Set.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.cachers_and_descendants
|
99
|
+
all_cachers = Set.new
|
100
|
+
Cacheable.cachers.each do |c|
|
101
|
+
all_cachers << c
|
102
|
+
all_cachers += c.descendants if c.is_a? Class
|
103
|
+
end
|
104
|
+
all_cachers
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.warm(*args)
|
108
|
+
begin
|
109
|
+
Cacheable.cache_options_threaded = {force: true}
|
110
|
+
Cacheable.cachers_and_descendants.each { |cacher| cacher.cache_warm(*args) }
|
111
|
+
ensure
|
112
|
+
Cacheable.cache_options_threaded = nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def self.default_cache
|
118
|
+
rails_cache ||
|
119
|
+
active_support_cache ||
|
120
|
+
raise(
|
121
|
+
<<-ERR_TEXT
|
122
|
+
When using Cacheable in a project that does not have a Rails or ActiveSupport Cache,
|
123
|
+
you must explicitly set the cache to an object shaped like ActiveSupport::Cache::Store
|
124
|
+
ERR_TEXT
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.rails_cache
|
129
|
+
defined?(Rails) && Rails.cache
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.active_support_cache
|
133
|
+
defined?(ActiveSupport) &&
|
134
|
+
defined?(ActiveSupport::Cache) &&
|
135
|
+
defined?(ActiveSupport::Cache::MemoryStore) &&
|
136
|
+
ActiveSupport::Cache::MemoryStore.new
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ForgetMeNot
|
2
|
+
# Allows the memoization of method calls
|
3
|
+
module Memoizable
|
4
|
+
class << self
|
5
|
+
def included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def memoize(*methods)
|
12
|
+
options = methods.last.is_a?(Hash) ? methods.pop : {}
|
13
|
+
methods.each { |m| memoize_method(m, options) }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def memoize_method(method_name, options)
|
18
|
+
method = instance_method(method_name)
|
19
|
+
visibility = method_visibility(method_name)
|
20
|
+
define_memoized_method(method, options)
|
21
|
+
send(visibility, method_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def define_memoized_method(method, options)
|
25
|
+
method_name = method.name.to_sym
|
26
|
+
key_prefix = "/memoized_method_result/#{self.name}"
|
27
|
+
|
28
|
+
undef_method(method_name)
|
29
|
+
define_method(method_name) do |*args|
|
30
|
+
memoize_key = [
|
31
|
+
key_prefix,
|
32
|
+
method_name,
|
33
|
+
args.hash
|
34
|
+
].compact.join '/'
|
35
|
+
|
36
|
+
puts "key: #{memoize_key}" if (defined?(Rails) && Rails.env.test?)
|
37
|
+
|
38
|
+
fetch_from_storage(memoize_key) do
|
39
|
+
method.bind(self).call(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def method_visibility(method)
|
45
|
+
if private_method_defined?(method)
|
46
|
+
:private
|
47
|
+
elsif protected_method_defined?(method)
|
48
|
+
:protected
|
49
|
+
else
|
50
|
+
:public
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_from_storage(key, &block)
|
56
|
+
storage.fetch(key, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def storage
|
60
|
+
@storage ||= Memoizable.storage_builder.call
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
def storage_builder
|
65
|
+
@storage_builder ||= Proc.new {HashCache.new}
|
66
|
+
end
|
67
|
+
|
68
|
+
def storage_builder=(builder)
|
69
|
+
@storage_builder = builder
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ForgetMeNot
|
4
|
+
class TestClass
|
5
|
+
include Cacheable
|
6
|
+
include MethodCounter
|
7
|
+
|
8
|
+
def method1
|
9
|
+
record_call :method1
|
10
|
+
'method1'
|
11
|
+
end
|
12
|
+
|
13
|
+
def method2(arg)
|
14
|
+
record_call :method2
|
15
|
+
"method2(#{arg})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def method3(org, month, visit_type)
|
19
|
+
record_call :method3
|
20
|
+
"method3(#{org}, #{month}, #{visit_type})"
|
21
|
+
end
|
22
|
+
|
23
|
+
def method4(array_arg)
|
24
|
+
record_call :method4
|
25
|
+
"method4([#{array_arg.join ','}])"
|
26
|
+
end
|
27
|
+
|
28
|
+
def method5(hash_arg)
|
29
|
+
record_call :method5
|
30
|
+
"method5({#{hash_arg.map { |k, v| "#{k}/#{v}" }.join '|' }})"
|
31
|
+
end
|
32
|
+
|
33
|
+
cache_results :method1, :method2, :method3, :method4, :method5
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class TestClass2
|
38
|
+
include Cacheable
|
39
|
+
include MethodCounter
|
40
|
+
|
41
|
+
attr_reader :org, :month
|
42
|
+
|
43
|
+
def initialize(org, month)
|
44
|
+
@org = org
|
45
|
+
@month = month
|
46
|
+
end
|
47
|
+
|
48
|
+
def method1
|
49
|
+
record_call :method1
|
50
|
+
"[#{org},#{month}].method1"
|
51
|
+
end
|
52
|
+
|
53
|
+
cache_results :method1, include: [:org, :month]
|
54
|
+
end
|
55
|
+
|
56
|
+
class TestClass3 < TestClass2
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
describe Cacheable do
|
61
|
+
before do
|
62
|
+
Cacheable.cache = ActiveSupport::Cache::MemoryStore.new
|
63
|
+
TestClass.clear_calls
|
64
|
+
TestClass2.clear_calls
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
describe 'cache arity-0 calls' do
|
69
|
+
it 'should cache calls from the same instance' do
|
70
|
+
foo = TestClass.new
|
71
|
+
expect(foo.method1).to eq 'method1'
|
72
|
+
expect(foo.method1).to eq 'method1'
|
73
|
+
|
74
|
+
expect(TestClass.count(:method1)).to eq 1
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should cache calls from the different instances' do
|
78
|
+
foo = TestClass.new
|
79
|
+
bar = TestClass.new
|
80
|
+
expect(foo.method1).to eq 'method1'
|
81
|
+
expect(bar.method1).to eq 'method1'
|
82
|
+
|
83
|
+
expect(TestClass.count(:method1)).to eq 1
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'cache should expire' do
|
87
|
+
foo = TestClass.new
|
88
|
+
expect(foo.method1).to eq 'method1'
|
89
|
+
|
90
|
+
Timecop.freeze(DateTime.now + 13.hours) do
|
91
|
+
expect(foo.method1).to eq 'method1'
|
92
|
+
end
|
93
|
+
|
94
|
+
expect(TestClass.count(:method1)).to eq 2
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'cache arity-1 calls' do
|
99
|
+
it 'should cache calls from the same instance, same argument' do
|
100
|
+
foo = TestClass.new
|
101
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
102
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
103
|
+
|
104
|
+
expect(TestClass.count(:method2)).to eq 1
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should cache calls from the same instance, different arguments' do
|
108
|
+
foo = TestClass.new
|
109
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
110
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
111
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
112
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
113
|
+
|
114
|
+
expect(TestClass.count(:method2)).to eq 2
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should distinguish calls with null and blank arguments' do
|
118
|
+
foo = TestClass.new
|
119
|
+
expect(foo.method2(nil)).to eq 'method2()'
|
120
|
+
expect(foo.method2(nil)).to eq 'method2()'
|
121
|
+
expect(foo.method2('')).to eq 'method2()'
|
122
|
+
expect(foo.method2('')).to eq 'method2()'
|
123
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
124
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
125
|
+
|
126
|
+
expect(TestClass.count(:method2)).to eq 3
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should distinguish calls with different elements in an array argument' do
|
130
|
+
foo = TestClass.new
|
131
|
+
expect(foo.method4([1, 2])).to eq 'method4([1,2])'
|
132
|
+
expect(foo.method4([1, 2])).to eq 'method4([1,2])'
|
133
|
+
expect(foo.method4([1, 3])).to eq 'method4([1,3])'
|
134
|
+
expect(foo.method4([1, 3])).to eq 'method4([1,3])'
|
135
|
+
expect(TestClass.count(:method4)).to eq 2
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should distinguish calls with different elements in a hash argument' do
|
139
|
+
foo = TestClass.new
|
140
|
+
expect(foo.method5(foo: 1, bar: 2)).to eq 'method5({foo/1|bar/2})'
|
141
|
+
expect(foo.method5(foo: 1, bar: 2)).to eq 'method5({foo/1|bar/2})'
|
142
|
+
|
143
|
+
expect(foo.method5(foo: 1, bar: 3)).to eq 'method5({foo/1|bar/3})'
|
144
|
+
expect(foo.method5(foo: 1, bar: 3)).to eq 'method5({foo/1|bar/3})'
|
145
|
+
|
146
|
+
expect(foo.method5(foo: 1, elvis: 2)).to eq 'method5({foo/1|elvis/2})'
|
147
|
+
expect(foo.method5(foo: 1, elvis: 2)).to eq 'method5({foo/1|elvis/2})'
|
148
|
+
|
149
|
+
expect(TestClass.count(:method5)).to eq 3
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'cache arity-3 calls' do
|
155
|
+
it 'should cache calls from the same instance, different arguments' do
|
156
|
+
foo = TestClass.new
|
157
|
+
expect(foo.method3('general', 201312, :emergency)).to eq 'method3(general, 201312, emergency)'
|
158
|
+
expect(foo.method3('general', 201312, :emergency)).to eq 'method3(general, 201312, emergency)'
|
159
|
+
expect(foo.method3('general', 201311, :emergency)).to eq 'method3(general, 201311, emergency)'
|
160
|
+
expect(foo.method3('general', 201311, :emergency)).to eq 'method3(general, 201311, emergency)'
|
161
|
+
|
162
|
+
expect(TestClass.count(:method3)).to eq 2
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe 'cache with instance args' do
|
167
|
+
it 'same instance parameters' do
|
168
|
+
foo = TestClass2.new('general', 201312)
|
169
|
+
bar = TestClass2.new('general', 201312)
|
170
|
+
expect(foo.method1).to eq '[general,201312].method1'
|
171
|
+
expect(bar.method1).to eq '[general,201312].method1'
|
172
|
+
expect(TestClass2.count(:method1)).to eq 1
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'different instance parameters' do
|
176
|
+
foo = TestClass2.new('general', 201312)
|
177
|
+
bar = TestClass2.new('general', 201311)
|
178
|
+
expect(foo.method1).to eq '[general,201312].method1'
|
179
|
+
expect(bar.method1).to eq '[general,201311].method1'
|
180
|
+
expect(TestClass2.count(:method1)).to eq 2
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe 'cachers' do
|
185
|
+
|
186
|
+
it 'should track objects that include Cacheable' do
|
187
|
+
expected = [TestClass, TestClass2].to_set
|
188
|
+
expect(Cacheable.cachers & expected).to eq expected
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'cachers_and_descendants should include descendants' do
|
192
|
+
expected = [TestClass, TestClass2, TestClass3].to_set
|
193
|
+
expect(Cacheable.cachers_and_descendants & expected).to eq expected
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ForgetMeNot
|
4
|
+
class MemoizeTestClass
|
5
|
+
include Memoizable
|
6
|
+
include MethodCounter
|
7
|
+
|
8
|
+
def method1
|
9
|
+
record_call :method1
|
10
|
+
'method1'
|
11
|
+
end
|
12
|
+
|
13
|
+
def method2(arg)
|
14
|
+
record_call :method2
|
15
|
+
"method2(#{arg})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def method3(org, month, visit_type)
|
19
|
+
record_call :method3
|
20
|
+
"method3(#{org}, #{month}, #{visit_type})"
|
21
|
+
end
|
22
|
+
|
23
|
+
def method4(array_arg)
|
24
|
+
record_call :method4
|
25
|
+
"method4([#{array_arg.join ','}])"
|
26
|
+
end
|
27
|
+
|
28
|
+
def method5(hash_arg)
|
29
|
+
record_call :method5
|
30
|
+
"method5({#{hash_arg.map { |k, v| "#{k}/#{v}" }.join '|' }})"
|
31
|
+
end
|
32
|
+
|
33
|
+
memoize :method1, :method2, :method3, :method4, :method5
|
34
|
+
end
|
35
|
+
|
36
|
+
describe Memoizable do
|
37
|
+
before do
|
38
|
+
MemoizeTestClass.clear_calls
|
39
|
+
TestClass2.clear_calls
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
describe 'memoize arity-0 calls' do
|
44
|
+
it 'should memoize calls from the same instance' do
|
45
|
+
foo = MemoizeTestClass.new
|
46
|
+
expect(foo.method1).to eq 'method1'
|
47
|
+
expect(foo.method1).to eq 'method1'
|
48
|
+
|
49
|
+
expect(MemoizeTestClass.count(:method1)).to eq 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should memoize calls from the different instances separately' do
|
53
|
+
foo = MemoizeTestClass.new
|
54
|
+
bar = MemoizeTestClass.new
|
55
|
+
expect(foo.method1).to eq 'method1'
|
56
|
+
expect(bar.method1).to eq 'method1'
|
57
|
+
|
58
|
+
expect(MemoizeTestClass.count(:method1)).to eq 2
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'memoize arity-1 calls' do
|
64
|
+
it 'should memoize calls from the same instance, same argument' do
|
65
|
+
foo = MemoizeTestClass.new
|
66
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
67
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
68
|
+
|
69
|
+
expect(MemoizeTestClass.count(:method2)).to eq 1
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should memoize calls from the same instance, different arguments' do
|
73
|
+
foo = MemoizeTestClass.new
|
74
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
75
|
+
expect(foo.method2('foo')).to eq 'method2(foo)'
|
76
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
77
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
78
|
+
|
79
|
+
expect(MemoizeTestClass.count(:method2)).to eq 2
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should distinguish calls with null and blank arguments' do
|
83
|
+
foo = MemoizeTestClass.new
|
84
|
+
expect(foo.method2(nil)).to eq 'method2()'
|
85
|
+
expect(foo.method2(nil)).to eq 'method2()'
|
86
|
+
expect(foo.method2('')).to eq 'method2()'
|
87
|
+
expect(foo.method2('')).to eq 'method2()'
|
88
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
89
|
+
expect(foo.method2('bar')).to eq 'method2(bar)'
|
90
|
+
|
91
|
+
expect(MemoizeTestClass.count(:method2)).to eq 3
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should distinguish calls with different elements in an array argument' do
|
95
|
+
foo = MemoizeTestClass.new
|
96
|
+
expect(foo.method4([1, 2])).to eq 'method4([1,2])'
|
97
|
+
expect(foo.method4([1, 2])).to eq 'method4([1,2])'
|
98
|
+
expect(foo.method4([1, 3])).to eq 'method4([1,3])'
|
99
|
+
expect(foo.method4([1, 3])).to eq 'method4([1,3])'
|
100
|
+
expect(MemoizeTestClass.count(:method4)).to eq 2
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should distinguish calls with different elements in a hash argument' do
|
104
|
+
foo = MemoizeTestClass.new
|
105
|
+
expect(foo.method5(foo: 1, bar: 2)).to eq 'method5({foo/1|bar/2})'
|
106
|
+
expect(foo.method5(foo: 1, bar: 2)).to eq 'method5({foo/1|bar/2})'
|
107
|
+
|
108
|
+
expect(foo.method5(foo: 1, bar: 3)).to eq 'method5({foo/1|bar/3})'
|
109
|
+
expect(foo.method5(foo: 1, bar: 3)).to eq 'method5({foo/1|bar/3})'
|
110
|
+
|
111
|
+
expect(foo.method5(foo: 1, elvis: 2)).to eq 'method5({foo/1|elvis/2})'
|
112
|
+
expect(foo.method5(foo: 1, elvis: 2)).to eq 'method5({foo/1|elvis/2})'
|
113
|
+
|
114
|
+
expect(MemoizeTestClass.count(:method5)).to eq 3
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'memoize arity-3 calls' do
|
120
|
+
it 'should memoize calls from the same instance, different arguments' do
|
121
|
+
foo = MemoizeTestClass.new
|
122
|
+
expect(foo.method3('general', 201312, :emergency)).to eq 'method3(general, 201312, emergency)'
|
123
|
+
expect(foo.method3('general', 201312, :emergency)).to eq 'method3(general, 201312, emergency)'
|
124
|
+
expect(foo.method3('general', 201311, :emergency)).to eq 'method3(general, 201311, emergency)'
|
125
|
+
expect(foo.method3('general', 201311, :emergency)).to eq 'method3(general, 201311, emergency)'
|
126
|
+
|
127
|
+
expect(MemoizeTestClass.count(:method3)).to eq 2
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'rspec'
|
4
|
+
|
5
|
+
require 'active_support/all'
|
6
|
+
require 'timecop'
|
7
|
+
|
8
|
+
require 'coveralls'
|
9
|
+
Coveralls.wear!
|
10
|
+
|
11
|
+
require 'forget-me-not'
|
12
|
+
|
13
|
+
|
14
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MethodCounter
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
module ClassMethods
|
4
|
+
def clear_calls
|
5
|
+
calls.clear
|
6
|
+
end
|
7
|
+
|
8
|
+
def calls
|
9
|
+
@@calls ||= Hash.new(0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def count(method_name)
|
13
|
+
calls[method_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def record_call(method_name)
|
17
|
+
calls[method_name] += 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def record_call(method_name)
|
22
|
+
self.class.record_call(method_name)
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forget-me-not
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Koan Health
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Caching and Memoization Mixins
|
84
|
+
email:
|
85
|
+
- development@koanhealth.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- lib/forget-me-not/cacheable.rb
|
91
|
+
- lib/forget-me-not/hash_cache.rb
|
92
|
+
- lib/forget-me-not/memoizable.rb
|
93
|
+
- lib/forget-me-not/version.rb
|
94
|
+
- lib/forget-me-not.rb
|
95
|
+
- CHANGELOG.md
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- spec/cacheable_spec.rb
|
100
|
+
- spec/memoizable_spec.rb
|
101
|
+
- spec/spec_helper.rb
|
102
|
+
- spec/support/method_counter.rb
|
103
|
+
homepage: https://github.com/KoanHealth/forget-me-not
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '1.9'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 1.3.6
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.1.11
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Mixins that provide caching and memoization for Ruby classes
|
127
|
+
test_files:
|
128
|
+
- spec/cacheable_spec.rb
|
129
|
+
- spec/memoizable_spec.rb
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
- spec/support/method_counter.rb
|