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 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