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 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,12 @@
1
+ module ForgetMeNot
2
+ class HashCache
3
+ def fetch(key)
4
+ data[key] ||= yield
5
+ end
6
+
7
+ private
8
+ def data
9
+ @data ||= {}
10
+ end
11
+ end
12
+ 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,3 @@
1
+ module ForgetMeNot
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'forget-me-not/version'
2
+
3
+ require 'forget-me-not/cacheable'
4
+ require 'forget-me-not/memoizable'
5
+ require 'forget-me-not/hash_cache'
@@ -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
@@ -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