cache_it 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *~
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cache_it.gemspec
4
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Rodrigo Vanegas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,23 @@
1
+ cache_it
2
+ ========
3
+
4
+ Cache for ActiveRecord objects, backed by ActiveSupport::CacheStore of your choice. Cache-money was
5
+ not yet ported to Rails 3 so I rolled my own.
6
+
7
+
8
+ Example
9
+ =======
10
+
11
+ class User < ActiveRecord::Base
12
+ cache_it do |c|
13
+ c.index :first, :last
14
+ c.index :email
15
+ end
16
+ end
17
+
18
+ user = User.cache_it_find :first => "Joe", :last => "Schmoe"
19
+ user = User.cache_it_find :email => "joe@example.com"
20
+ user.age = 30
21
+ user.cache_it_write
22
+
23
+ Copyright (c) 2011 Rodrigo Vanegas, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc 'Default: run specs.'
7
+ task :default => :spec
8
+
9
+ desc "Run specs"
10
+ RSpec::Core::RakeTask.new do |t|
11
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
12
+ # Put spec opts in a file named .rspec in root
13
+ end
14
+
15
+ desc "Generate code coverage"
16
+ RSpec::Core::RakeTask.new(:coverage) do |t|
17
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
18
+ t.rcov = true
19
+ t.rcov_opts = ['--exclude', 'spec']
20
+ end
21
+
data/cache_it.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cache_it/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cache_it"
7
+ s.version = CacheIt::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Rodrigo Vanegas"]
10
+ s.email = ["rvanegas@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{ActiveRecord caching}
13
+ s.description = %q{Integrates ActiveRecord with cache stores provided by Rails.cache, incluing memcached}
14
+
15
+ s.rubyforge_project = "cache_it"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'cache_it'
2
+
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,3 @@
1
+ module CacheIt
2
+ VERSION = "0.0.1"
3
+ end
data/lib/cache_it.rb ADDED
@@ -0,0 +1,228 @@
1
+
2
+ module ActiveRecord
3
+ class Base
4
+ #
5
+ # First time called, configures \cache_it for this ActiveRecord model.
6
+ # Subsequent calls returns a delegate to access class methods.
7
+ #
8
+ # Examples:
9
+ #
10
+ # class Foo < ActiveRecord::Base
11
+ # cache_it :first, :last
12
+ # end
13
+ #
14
+ # class Foo < ActiveRecord::Base
15
+ # cache_it do |c|
16
+ # c.index :first, :last
17
+ # c.index :email
18
+ # c.counters :points
19
+ # end
20
+ # end
21
+ #
22
+ def self.cache_it(*index)
23
+ if self.class_variable_defined? :@@cache_it
24
+ raise ArgumentError, "cannot reconfigure" if index.present? or block_given?
25
+ else
26
+ raise ArgumentError, "use block or args" if index.present? and block_given?
27
+ config = CacheIt::Config.new self
28
+ config.index *index if config and index.present?
29
+ yield config if config and block_given?
30
+ self.class_exec do
31
+ # Returns delegate to access instance methods
32
+ def cache_it
33
+ @cache_it ||= CacheIt::InstanceDelegate.new self
34
+ end
35
+ end
36
+ delegate = CacheIt::ClassDelegate.new self, config
37
+ self.class_variable_set "@@cache_it", delegate
38
+ end
39
+ self.class_variable_get "@@cache_it"
40
+ end
41
+ end
42
+ end
43
+
44
+ #
45
+ # Cache for ActiveRecord objects, backed by ActiveSupport::CacheStore of your choice.
46
+ #
47
+ # ==Usage
48
+ #
49
+ # # migration
50
+ # create_table :users do |t|
51
+ # t.string :first
52
+ # t.string :last
53
+ # t.string :email
54
+ # t.integer :age
55
+ # t.integer :points
56
+ # end
57
+ #
58
+ # class User < ActiveRecord::Base
59
+ # cache_it do |c|
60
+ # c.index :first, :last
61
+ # c.index :email
62
+ # c.counters :points
63
+ # end
64
+ # end
65
+ #
66
+ # user = User.cache_it.find(:first => "Joe", :last => "Schmoe")
67
+ # user = User.cache_it.find(:email => "joe@example.com")
68
+ # user.age = 30
69
+ # user.cache_it_write
70
+ #
71
+ module CacheIt
72
+ class InstanceDelegate
73
+ def initialize(base)
74
+ @base = base
75
+ end
76
+
77
+ def write
78
+ expires_in = @base.class.cache_it.config.expires_in
79
+ val = {:attributes => @base.attributes}
80
+ keys.each {|key| Rails.cache.write(key, val, :expires_in => expires_in)}
81
+ stale_keys.each {|key| Rails.cache.delete(key)}
82
+ init_counters
83
+ end
84
+
85
+ def increment(counter, amount = 1)
86
+ counter = counter.to_s
87
+ unless @base.class.cache_it.config.counters.include? counter
88
+ raise ArgumentError, "#{counter} is not a counter"
89
+ end
90
+ primary_key = @base.class.primary_key
91
+ if key = @base.class.cache_it.key({primary_key => @base[primary_key]}, :counter => counter)
92
+ @base[counter] = Rails.cache.increment(key, amount, :raw => true)
93
+ end
94
+ end
95
+
96
+ def delete
97
+ keys(attributes_before_changes).each {|key| Rails.cache.delete(key)}
98
+ end
99
+
100
+ def init_counters
101
+ primary_key = @base.class.primary_key
102
+ @base.class.cache_it.config.counters.map do |counter|
103
+ counter_key = @base.class.cache_it.key({primary_key => @base[primary_key]}, :counter => counter)
104
+ @base[counter] = Rails.cache.fetch(counter_key, :raw => true) { @base[counter] }
105
+ end
106
+ end
107
+
108
+ private
109
+ def attributes_before_changes
110
+ result = Hash.new
111
+ @base.attributes.each do |k,v|
112
+ result[k] = @base.changes.include?(k) ? @base.changes[k].first : v
113
+ end
114
+ return result
115
+ end
116
+
117
+ def keys(attrs = @base.attributes)
118
+ @base.class.cache_it.config.indexes.map do |index|
119
+ @base.class.cache_it.key attrs.select {|attr| index.include? attr}
120
+ end
121
+ end
122
+
123
+ def stale_keys
124
+ keys(attributes_before_changes) - keys(@base.attributes)
125
+ end
126
+ end
127
+
128
+ class ClassDelegate
129
+ def initialize(base, config)
130
+ @base = base
131
+ @config = config
132
+ @base.after_save Proc.new { cache_it.write }
133
+ @base.after_destroy Proc.new { cache_it.delete }
134
+ end
135
+
136
+ def key(attrs, options = {})
137
+ attrs = attrs.stringify_keys
138
+ index = attrs.keys.sort
139
+ raise ArgumentError, "index not available" unless @config.indexes.include? index
140
+ if options[:counter]
141
+ raise ArgumentError, "not a counter" unless @config.counters.include? options[:counter]
142
+ end
143
+ key = ["CacheIt.v1", @base.name]
144
+ key.push options[:counter] if options[:counter]
145
+ key.push index.map{|name| [name, attrs[name]]}.to_json
146
+ return key.join(":")
147
+ end
148
+
149
+ def find(attrs, options = {})
150
+ unless obj = read(attrs, options)
151
+ obj = @base.where(attrs).first
152
+ obj.cache_it.write if obj
153
+ end
154
+ return obj
155
+ end
156
+
157
+ def read(attrs, options = {})
158
+ key = key(attrs)
159
+ obj = nil
160
+ if val = Rails.cache.read(key)
161
+ attributes = val[:attributes]
162
+ obj = @base.new
163
+ obj.send :attributes=, attributes, false
164
+ obj.instance_variable_set("@new_record", false) if obj.id
165
+ obj.cache_it.init_counters unless options[:skip_counters]
166
+ end
167
+ return obj
168
+ end
169
+
170
+ def config
171
+ @config
172
+ end
173
+ end
174
+
175
+ class Config
176
+ def initialize(model)
177
+ @model = model
178
+ @indexes ||= [[@model.primary_key]]
179
+ @counters ||= []
180
+ end
181
+
182
+ def index(*index)
183
+ return nil unless index.present?
184
+ index = index.map {|n|n.to_s}.sort
185
+ unless index.all? {|n| @model.column_names.include? n}
186
+ raise ArgumentError, "index must be list of column names"
187
+ end
188
+ @indexes.push index unless @indexes.include? index
189
+ validate
190
+ return nil
191
+ end
192
+
193
+ def indexes
194
+ @indexes
195
+ end
196
+
197
+ def counters(*counters)
198
+ return @counters unless counters.present?
199
+ counters = counters.map {|n|n.to_s}
200
+ unless counters.all? {|n| @model.columns_hash[n].try(:type) == :integer}
201
+ raise ArgumentError, "counters must be column names for integer attributes"
202
+ end
203
+ counters.each do |name|
204
+ @counters.push name unless @counters.include? name
205
+ end
206
+ validate
207
+ return nil
208
+ end
209
+
210
+ def expires_in(expires_in = nil, &block)
211
+ unless expires_in or block_given?
212
+ @expires_in.respond_to?(:call) ? @expires_in.call : @expires_in
213
+ else
214
+ raise ArgumentError, "use block or args" if expires_in and block_given?
215
+ @expires_in = expires_in || block
216
+ return nil
217
+ end
218
+ end
219
+
220
+ private
221
+ def validate
222
+ if @indexes.flatten.any? {|name| @counters.include? name}
223
+ raise "cannot use column for both index and counter"
224
+ end
225
+ end
226
+ end
227
+ end
228
+
@@ -0,0 +1,232 @@
1
+
2
+ require 'ruby-debug'
3
+ Debugger.start
4
+
5
+ require 'sqlite3'
6
+ require 'active_record'
7
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/cache_it.rb')
8
+
9
+ class MockCache
10
+ def initialize
11
+ clear
12
+ end
13
+
14
+ def write(key, val, options = {})
15
+ @hash[key] = val
16
+ end
17
+
18
+ def read(key)
19
+ @hash[key]
20
+ end
21
+
22
+ def fetch(key, options = {})
23
+ if @hash.has_key?(key)
24
+ @hash[key]
25
+ elsif block_given?
26
+ @hash[key] = yield
27
+ end
28
+ end
29
+
30
+ def increment(key, amount = 1, options = {})
31
+ @hash[key] = @hash[key].to_s.to_i + amount
32
+ end
33
+
34
+ def delete(key)
35
+ @hash.delete(key)
36
+ end
37
+
38
+ def clear
39
+ @hash = Hash.new
40
+ end
41
+ end
42
+
43
+ describe MockCache do
44
+ it "writes and reads" do
45
+ subject.read("foo").should== nil
46
+ subject.write("foo", 1)
47
+ subject.read("foo").should== 1
48
+ end
49
+
50
+ it "fetches" do
51
+ subject.read("foo").should== nil
52
+ subject.fetch("foo", 1){ 1 }.should== 1
53
+ subject.read("foo").should== 1
54
+ subject.fetch("foo", 1).should== 1
55
+ end
56
+
57
+ it "increments" do
58
+ subject.read("foo").should== nil
59
+ subject.increment("foo")
60
+ subject.increment("foo", 2)
61
+ subject.write("foo", 1)
62
+ subject.increment("foo")
63
+ subject.increment("foo", 2)
64
+ subject.read("foo").should== 4
65
+ end
66
+
67
+ it "deletes" do
68
+ subject.read("foo").should== nil
69
+ subject.write("foo", 1)
70
+ subject.read("foo").should== 1
71
+ subject.delete("foo")
72
+ subject.read("foo").should== nil
73
+ end
74
+ end
75
+
76
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
77
+
78
+ silence_stream(STDOUT) do
79
+ ActiveRecord::Schema.define(:version => 1) do
80
+ create_table :users do |t|
81
+ t.string :code
82
+ t.string :name
83
+ t.integer :points, :default => 0
84
+ end
85
+ end
86
+ end
87
+
88
+ class Rails
89
+ @@cache = MockCache.new
90
+ def self.cache
91
+ @@cache
92
+ end
93
+ end
94
+
95
+ class User < ActiveRecord::Base
96
+ cache_it do |c|
97
+ c.index :code
98
+ c.index :name
99
+ c.counters :points
100
+ end
101
+ end
102
+
103
+ describe CacheIt do
104
+ context "User" do
105
+ before do
106
+ User.delete_all
107
+ Rails.cache.clear
108
+ @u = User.create(:code => "x", :name => "joe")
109
+ end
110
+
111
+ it "reads" do
112
+ User.cache_it.read(:name => "joe").should== @u
113
+ end
114
+
115
+ it "writes" do
116
+ @u.name = "jane"
117
+ @u.cache_it.write
118
+ User.cache_it.read(:name => "jane").should== @u
119
+ User.find_by_sql("select * from users where id = #{@u.id}").first.name.should== "joe"
120
+ @u.save!
121
+ User.find_by_sql("select * from users where id = #{@u.id}").first.name.should== "jane"
122
+ end
123
+
124
+ it "increments" do
125
+ @u.cache_it.increment(:points)
126
+ @u.points.should== 1
127
+ User.find_by_sql("select * from users where name = 'joe'").first.points.should== 0
128
+ @u.save!
129
+ User.find_by_sql("select * from users where name = 'joe'").first.points.should== 1
130
+ end
131
+
132
+ it "deletes stale keys" do
133
+ @u.code = "y"
134
+ @u.save!
135
+ User.cache_it.read(:code => "x").should== nil
136
+ end
137
+
138
+ it "deletes stale keys on destroy" do
139
+ @u.code = "y"
140
+ @u.destroy
141
+ User.cache_it.read(:code => "x").should== nil
142
+ end
143
+
144
+ it "syncs counters" do
145
+ @u.cache_it.increment(:points)
146
+ @u2 = User.cache_it.read(:name => "joe")
147
+ @u2.points.should== 1
148
+ @u2.cache_it.increment(:points)
149
+ @u3 = User.cache_it.read({:name => "joe"}, :skip_counters => true)
150
+ @u3.points.should== 0
151
+ @u4 = User.cache_it.read(:name => "joe")
152
+ @u4.points.should== 2
153
+ end
154
+
155
+ it "nil for read of unknown keys" do
156
+ User.cache_it.read(:name => "dave").should== nil
157
+ end
158
+
159
+ it "flags set right" do
160
+ @u2 = User.cache_it.read(:name => "joe")
161
+ @u2.new_record?.should== false
162
+ @u2.persisted?.should== true
163
+ end
164
+
165
+ it "doesn't accept unknown index" do
166
+ expect { User.cache_it.read(:points => 10) }.to raise_error(/index not available/)
167
+ end
168
+ end
169
+
170
+ context "config" do
171
+ before do
172
+ @users_class = Class.new ActiveRecord::Base
173
+ @users_class.set_table_name "users"
174
+ end
175
+
176
+ it "can't use same column for both index and counter" do
177
+ expect do
178
+ @users_class.cache_it do |c|
179
+ c.index :name, :points
180
+ c.counters :points
181
+ end
182
+ end.to raise_error(/cannot use column/)
183
+ end
184
+
185
+ it "can use arg" do
186
+ expect do
187
+ @users_class.cache_it :code
188
+ end.to_not raise_error
189
+ end
190
+
191
+ it "can't use arg and block" do
192
+ expect do
193
+ @users_class.cache_it(:code) {|c| c.index :name}
194
+ end.to raise_error(/block or args/)
195
+ end
196
+
197
+ it "accepts constant for expires" do
198
+ expect do
199
+ @users_class.cache_it {|c| c.expires_in 3}
200
+ end.to_not raise_error
201
+ end
202
+
203
+ it "accepts proc for expires" do
204
+ expect do
205
+ @users_class.cache_it {|c| c.expires_in { 3 }}
206
+ end.to_not raise_error
207
+ end
208
+
209
+ it "counter must be integer column" do
210
+ expect do
211
+ @users_class.cache_it {|c| c.counters :name}
212
+ end.to raise_error(/must be column names for integer/)
213
+ end
214
+
215
+ it "counter must be existing column" do
216
+ expect do
217
+ @users_class.cache_it {|c| c.counters :not_a_column}
218
+ end.to raise_error(/must be column names for integer/)
219
+ end
220
+
221
+ it "each class gets its own config" do
222
+ @users_class2 = Class.new ActiveRecord::Base
223
+ @users_class2.set_table_name "users"
224
+ @users_class2.cache_it.config.should_not== @users_class.cache_it.config
225
+ end
226
+
227
+ it "cannot config twice" do
228
+ expect {@users_class.cache_it :name}.to_not raise_error
229
+ expect {@users_class.cache_it :code}.to raise_error
230
+ end
231
+ end
232
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cache_it
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Rodrigo Vanegas
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-04-11 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Integrates ActiveRecord with cache stores provided by Rails.cache, incluing memcached
22
+ email:
23
+ - rvanegas@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - .gitignore
32
+ - Gemfile
33
+ - MIT-LICENSE
34
+ - README
35
+ - Rakefile
36
+ - cache_it.gemspec
37
+ - init.rb
38
+ - install.rb
39
+ - lib/cache_it.rb
40
+ - lib/cache_it/version.rb
41
+ - spec/cache_it_spec.rb
42
+ - uninstall.rb
43
+ has_rdoc: true
44
+ homepage: ""
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ requirements: []
69
+
70
+ rubyforge_project: cache_it
71
+ rubygems_version: 1.3.7
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: ActiveRecord caching
75
+ test_files: []
76
+