forget-me-not 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/KoanHealth/forget-me-not.png?branch=master&.png)](http://travis-ci.org/KoanHealth/forget-me-not)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/KoanHealth/forget-me-not.png)](https://codeclimate.com/github/KoanHealth/forget-me-not)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/KoanHealth/forget-me-not/badge.png?branch=master)](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
|