djmaze-arid_cache 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,86 @@
1
+ module AridCache
2
+ module Helpers
3
+
4
+ # Lookup something from the cache.
5
+ #
6
+ # If no block is provided, create one dynamically. If a block is
7
+ # provided, it is only used the first time it is encountered.
8
+ # This allows you to dynamically define your caches while still
9
+ # returning the results of your query.
10
+ #
11
+ # @return a WillPaginate::Collection if the options include :page,
12
+ # a Fixnum count if the request is for a count or the results of
13
+ # the ActiveRecord query otherwise.
14
+ def lookup(object, key, opts, &block)
15
+ if !block.nil?
16
+ define(object, key, opts, &block)
17
+ elsif key =~ /(.*)_count$/
18
+ if AridCache.store.has?(object, $1)
19
+ method_for_cached(object, $1, :fetch_count, key)
20
+ elsif object.respond_to?(key)
21
+ define(object, key, opts, :fetch_count)
22
+ elsif object.respond_to?($1)
23
+ define(object, $1, opts, :fetch_count, key)
24
+ else
25
+ raise ArgumentError.new("#{object} doesn't respond to #{key} or #{$1}! Cannot dynamically create query to get the count, please call with a block.")
26
+ end
27
+ elsif AridCache.store.has?(object, key)
28
+ method_for_cached(object, key, :fetch)
29
+ elsif object.respond_to?(key)
30
+ define(object, key, opts, &block)
31
+ else
32
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query, please call with a block.")
33
+ end
34
+ object.send("cached_#{key}", opts)
35
+ end
36
+
37
+ # Store the options and optional block for a call to the cache.
38
+ #
39
+ # If no block is provided, create one dynamically.
40
+ #
41
+ # @return an AridCache::Store::Blueprint.
42
+ def define(object, key, opts, fetch_method=:fetch, method_name=nil, &block)
43
+
44
+ # FIXME: Pass default options to store.add
45
+ # Pass nil in for now until we get the cache_ calls working.
46
+ # This means that the first time you define a dynamic cache
47
+ # (by passing in a block), the options you used are not
48
+ # stored in the blueprint and applied to each subsequent call.
49
+ #
50
+ # Otherwise we have a situation where a :limit passed in to the
51
+ # first call persists when no options are passed in on subsequent calls,
52
+ # but if a different :limit is passed in that limit is applied.
53
+ #
54
+ # I think in this scenario one would expect no limit to be applied
55
+ # if no options are passed in.
56
+ #
57
+ # When the cache_ methods are supported, those options should be
58
+ # remembered and applied to the collection however.
59
+ blueprint = AridCache.store.add_object_cache_configuration(object, key, nil, block)
60
+ method_for_cached(object, key, fetch_method, method_name)
61
+ blueprint
62
+ end
63
+
64
+ private
65
+
66
+ # Dynamically define a method on the given object or class to return cached results
67
+ def method_for_cached(object_or_class, key, fetch_method=:fetch, method_name=nil)
68
+ method_name = ("cached_" + (method_name || key)).gsub(/[^\w\!\?]/, '_')
69
+ method_body = <<-END
70
+ def #{method_name}(*args, &block)
71
+ opts = args.empty? ? {} : args.first
72
+
73
+ proxy = AridCache::CacheProxy.new(self, #{key.inspect}, opts, &block)
74
+ if opts[:clear] == true
75
+ proxy.clear_cached
76
+ else
77
+ proxy.#{fetch_method.to_s}
78
+ end
79
+ end
80
+ END
81
+ # Get the correct class
82
+ klass = object_or_class.is_a?(Class) ? object_or_class : object_or_class.class
83
+ klass.class_eval(method_body, __FILE__, __LINE__)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,125 @@
1
+ module AridCache
2
+ class Store < Hash
3
+
4
+ # AridCache::Store::Blueprint
5
+ #
6
+ # Stores options and blocks that are used to generate results for finds
7
+ # and counts.
8
+ Blueprint = Struct.new(:klass, :key, :opts, :proc) do
9
+
10
+ def initialize(klass, key, opts={}, proc=nil)
11
+ self.key = key
12
+ self.klass = klass
13
+ self.proc = proc
14
+ self.opts = opts
15
+ end
16
+
17
+ def klass=(object) # store the class name only
18
+ self['klass'] = object.is_a?(Class) ? object.name : object.class.name
19
+ end
20
+
21
+ def klass
22
+ self['klass'].constantize unless self['klass'].nil?
23
+ end
24
+
25
+ def opts=(value)
26
+ self['opts'] = value.symbolize_keys! unless !value.respond_to?(:symbolize_keys)
27
+ end
28
+
29
+ def opts
30
+ self['opts'] || {}
31
+ end
32
+
33
+ def proc(object=nil)
34
+ if self['proc'].nil? && !object.nil?
35
+ self['proc'] = key
36
+ else
37
+ self['proc']
38
+ end
39
+ end
40
+ end
41
+
42
+ #
43
+ # Capture cache configurations and blocks and store them in the store.
44
+ #
45
+ # A block is evaluated within the scope of this class. The blocks
46
+ # contains calls to methods which define the caches and give options
47
+ # for each cache.
48
+ #
49
+ # Don't instantiate directly. Rather instantiate the Instance- or
50
+ # ClassCacheConfiguration.
51
+ Configuration = Struct.new(:klass, :global_opts) do
52
+
53
+ def initialize(klass, global_opts={})
54
+ self.global_opts = global_opts
55
+ self.klass = klass
56
+ end
57
+
58
+ def method_missing(key, *args, &block)
59
+ opts = global_opts.merge(args.empty? ? {} : args.first)
60
+ case self
61
+ when InstanceCacheConfiguration
62
+ AridCache.store.add_instance_cache_configuration(klass, key, opts, block)
63
+ else
64
+ AridCache.store.add_class_cache_configuration(klass, key, opts, block)
65
+ end
66
+ end
67
+ end
68
+ class InstanceCacheConfiguration < Configuration; end #:nodoc:
69
+ class ClassCacheConfiguration < Configuration; end #:nodoc:
70
+
71
+ def has?(object, key)
72
+ self.include?(object_store_key(object, key))
73
+ end
74
+
75
+ # Empty the proc store
76
+ def delete!
77
+ delete_if { true }
78
+ end
79
+
80
+ def self.instance
81
+ @@singleton_instance ||= self.new
82
+ end
83
+
84
+ def find(object, key)
85
+ self[object_store_key(object, key)]
86
+ end
87
+
88
+ def add_instance_cache_configuration(klass, key, opts, proc)
89
+ add_generic_cache_configuration(instance_store_key(klass, key), klass, key, opts, proc)
90
+ end
91
+
92
+ def add_class_cache_configuration(klass, key, opts, proc)
93
+ add_generic_cache_configuration(class_store_key(klass, key), klass, key, opts, proc)
94
+ end
95
+
96
+ def add_object_cache_configuration(object, key, opts, proc)
97
+ add_generic_cache_configuration(object_store_key(object, key), object, key, opts, proc)
98
+ end
99
+
100
+ def find_or_create(object, key)
101
+ store_key = object_store_key(object, key)
102
+ if self.include?(store_key)
103
+ self[store_key]
104
+ else
105
+ self[store_key] = AridCache::Store::Blueprint.new(object, key)
106
+ end
107
+ end
108
+
109
+ protected
110
+
111
+ def add_generic_cache_configuration(store_key, *args)
112
+ AridCache.send(:method_for_cached, args[0], args[1].to_s) # TODO Right place for this?
113
+ self[store_key] = AridCache::Store::Blueprint.new(*args)
114
+ end
115
+
116
+ def initialize
117
+ end
118
+
119
+ def class_store_key(klass, key); klass.name.downcase + '-' + key.to_s; end
120
+ def instance_store_key(klass, key); klass.name.downcase.pluralize + '-' + key.to_s; end
121
+ def object_store_key(object, key)
122
+ case object; when Class; class_store_key(object, key); else; instance_store_key(object.class, key); end
123
+ end
124
+ end
125
+ end
data/lib/arid_cache.rb ADDED
@@ -0,0 +1,47 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
+
4
+ require 'arid_cache/helpers'
5
+ require 'arid_cache/store'
6
+ require 'arid_cache/active_record'
7
+ require 'arid_cache/cache_proxy'
8
+
9
+ module AridCache
10
+ extend AridCache::Helpers
11
+ class Error < StandardError; end #:nodoc:
12
+
13
+ def self.cache
14
+ AridCache::CacheProxy
15
+ end
16
+
17
+ def self.clear_caches
18
+ AridCache::CacheProxy.clear_caches
19
+ end
20
+
21
+ def self.clear_class_caches(object)
22
+ AridCache::CacheProxy.clear_class_caches(object)
23
+ end
24
+
25
+ def self.clear_instance_caches(object)
26
+ AridCache::CacheProxy.clear_instance_caches(object)
27
+ end
28
+
29
+ def self.store
30
+ AridCache::Store.instance
31
+ end
32
+
33
+ # The old method of including this module, if you don't want to
34
+ # extend active record. Just add 'include AridCache' to your
35
+ # model class.
36
+ def self.included(base)
37
+ base.send(:include, AridCache::ActiveRecord)
38
+ end
39
+
40
+ # Initializes AridCache for Rails.
41
+ #
42
+ # This method is called by `init.rb`,
43
+ # which is run by Rails on startup.
44
+ def self.init_rails
45
+ ::ActiveRecord::Base.send(:include, AridCache::ActiveRecord)
46
+ end
47
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe AridCache do
4
+ describe 'results' do
5
+ before :each do
6
+ Company.destroy_all
7
+ @company1 = Company.make(:name => 'a')
8
+ @company2 = Company.make(:name => 'b')
9
+ Company.class_caches do
10
+ ordered_by_name { Company.all(:order => 'name ASC') }
11
+ end
12
+ Company.clear_caches
13
+ end
14
+
15
+ it "order should match the original order" do
16
+ 3.times do |t|
17
+ results = Company.cached_ordered_by_name
18
+ results.size.should == 2
19
+ results[0].name.should == @company1.name
20
+ results[1].name.should == @company2.name
21
+ end
22
+ end
23
+
24
+ it "order should match the order option" do
25
+ 3.times do |t|
26
+ results = Company.cached_ordered_by_name(:order => 'name DESC')
27
+ results.size.should == 2
28
+ results[0].name.should == @company2.name
29
+ results[1].name.should == @company1.name
30
+ end
31
+ end
32
+
33
+ it "with order option should go to the database to order" do
34
+ lambda {
35
+ Company.cached_ordered_by_name(:order => 'name DESC')
36
+ }.should query(2)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ describe AridCache::CacheProxy::Result do
4
+ before :each do
5
+ class X; end
6
+ @result = AridCache::CacheProxy::Result.new
7
+ end
8
+
9
+ it "should set the klass from a class" do
10
+ @result.klass = X
11
+ @result.klass.should be(X)
12
+ end
13
+
14
+ it "should set the klass from an object" do
15
+ @result.klass = X.new
16
+ @result.klass.should be(X)
17
+ end
18
+
19
+ it "should store the klass as a string" do
20
+ @result.klass = X
21
+ @result[:klass].should == X.name
22
+ end
23
+
24
+ it "should not have ids if it's nil" do
25
+ @result.ids = nil
26
+ @result.has_ids?.should be_false
27
+ end
28
+
29
+ it "should have ids" do
30
+ @result.ids = [1,2,3]
31
+ @result.has_ids?.should be_true
32
+ end
33
+
34
+ it "should have ids even if the array is empty" do
35
+ @result.ids = []
36
+ @result.has_ids?.should be_true
37
+ end
38
+
39
+ it "should not have a count if it's nil" do
40
+ @result.count = nil
41
+ @result.has_count?.should be_false
42
+ end
43
+
44
+ it "should have a count" do
45
+ @result.count = 3
46
+ @result.has_count?.should be_true
47
+ end
48
+
49
+ it "should have a count even if it is zero" do
50
+ @result.count = 0
51
+ @result.has_count?.should be_true
52
+ end
53
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe AridCache::CacheProxy do
4
+ describe 'preserve_order' do
5
+ before :each do
6
+ @proxy = AridCache::CacheProxy.new(Company, 'dummy-key', {})
7
+ end
8
+
9
+ it "id column should be prefixed by the table name" do
10
+ ::ActiveRecord::Base.stubs(:is_mysql_adapter?).returns(true)
11
+ @proxy.send(:preserve_order, [1,2,3]).should =~ %r[#{Company.table_name}]
12
+ end
13
+
14
+ it "id column should be prefixed by the table name" do
15
+ ::ActiveRecord::Base.stubs(:is_mysql_adapter?).returns(false)
16
+ @proxy.send(:preserve_order, [1,2,3]).should =~ %r[#{Company.table_name}]
17
+ end
18
+ end
19
+
20
+ describe "with raw => true" do
21
+ before :each do
22
+ @user = User.make
23
+ @user.companies << Company.make
24
+ @user.companies << Company.make
25
+ @user.clear_instance_caches
26
+ end
27
+
28
+ it "should return a CacheProxy::Result" do
29
+ @user.cached_companies(:raw => true).should be_a(AridCache::CacheProxy::Result)
30
+ end
31
+
32
+ it "result should have the same ids as the normal result" do
33
+ @user.cached_companies(:raw => true).ids.should == @user.cached_companies.collect(&:id)
34
+ end
35
+
36
+ it "should ignore :raw => false" do
37
+ @user.cached_companies(:raw => false).should == @user.cached_companies
38
+ end
39
+
40
+ it "should only query once to seed the cache, ignoring all other options" do
41
+ lambda { @user.cached_companies(:raw => true, :limit => 0, :order => 'nonexistent_column desc') }.should query(1)
42
+ end
43
+
44
+ it "should ignore all other options if the cache has already been seeded" do
45
+ lambda {
46
+ companies = @user.cached_companies
47
+ @user.cached_companies(:raw => true, :limit => 0, :order => 'nonexistent_column').ids.should == companies.collect(&:id)
48
+ }.should query(1)
49
+ end
50
+
51
+ it "should not use the raw option when reading from the cache" do
52
+ Rails.cache.expects(:read).with(@user.arid_cache_key(:companies), {})
53
+ @user.cached_companies(:raw => true)
54
+ end
55
+
56
+ it "should work for calls to a method that ends with _count" do
57
+ @user.cached_bogus_count do
58
+ 10
59
+ end
60
+ @user.cached_bogus_count(:raw => true).should == 10
61
+ end
62
+
63
+ it "should work for calls to a method that ends with _count" do
64
+ @user.cached_companies_count(:raw => true).should == @user.cached_companies_count
65
+ end
66
+ end
67
+
68
+ describe "with clear => true" do
69
+ before :each do
70
+ @user = User.make
71
+ @user.companies << Company.make
72
+ @user.companies << Company.make
73
+ @user.clear_instance_caches rescue Rails.cache.clear
74
+ end
75
+
76
+ it "should not fail if there is no cached value" do
77
+ lambda { @user.cached_companies(:clear => true) }.should_not raise_exception
78
+ end
79
+
80
+ it "should clear the cached entry" do
81
+ key = @user.arid_cache_key(:companies)
82
+ @user.cached_companies
83
+ Rails.cache.read(key).should_not be_nil
84
+ @user.cached_companies(:clear => true)
85
+ Rails.cache.read(key).should be_nil
86
+ end
87
+
88
+ it "should not read from the cache or database" do
89
+ Rails.cache.expects(:read).never
90
+ lambda {
91
+ @user.cached_companies(:clear => true)
92
+ }.should query(0)
93
+ end
94
+ end
95
+ end