cache_it 0.0.1

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