djmaze-arid_cache 1.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.
@@ -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