arid_cache 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ test/**/*.log
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Karl Varga
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.rdoc ADDED
@@ -0,0 +1,121 @@
1
+ == ARID Cache
2
+
3
+ ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
4
+
5
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
6
+
7
+ === Counts for free
8
+
9
+ ARID Cache gives you counts for free. When a large collection is stored in the cache
10
+ ARID Cache stores the count as well so the next time you want request the count it
11
+ just takes a single read from the cache. This is also supported for your non-ActiveRecord
12
+ collections if the collection <tt>responds_to?(:count)</tt>.
13
+
14
+ Given that we have a cache like <tt>album.cached_tracks</tt> we can get the count by calling <tt>album.cached_tracks_count</tt>.
15
+
16
+ === Auto-expiring cache keys
17
+
18
+ Caches on model instances automatically incorporate the ActiveRecord <tt>cache_key</tt> which includes the <tt>updated_at</tt> timestamp of that instance, making them auto-expire when the instance is updated.
19
+
20
+ Caches on your model classes (like on the results of named scopes) will not expire however.
21
+
22
+ ARID cache provides methods to help you expire your caches.
23
+
24
+ AridCache.clear_all_caches => expires all ARID Cache caches
25
+ Model.clear_all_caches => expires class and instance-level caches for this model
26
+ Model.clear_instance_caches => expires instance-level caches for this model
27
+ Model.clear_class_caches => expires class-level caches for this model
28
+
29
+ These methods are also available on model instances.
30
+
31
+ ARID Cache keys are based on the method you call to create the cache. For example:
32
+ Album.cached_featured_albums => cache key is arid-cache-album-featured_albums
33
+ album.cached_top_tracks => cache key like arid-cache-albums/2-20090930200-top_tracks
34
+
35
+ === Support for pagination and options to <tt>find</tt>
36
+
37
+ ARID cache performs pagination and applies <tt>:limit</tt> and <tt>:offset</tt> to the IDs in memory and only selects the page/sub-set from the database, directly from the target table.
38
+
39
+ You can pass options like <tt>:include</tt> (or any other valid <tt>find</tt> options) to augment the results of your cached query.
40
+
41
+ === Efficiency
42
+
43
+ ARID Cache intercepts calls to <tt>cached_</tt> methods using <tt>method_missing</tt> then defines those methods on your models as they are called, so they bypass method missing on subsequent calls.
44
+
45
+ == Examples
46
+
47
+ ==== Given the following model:
48
+
49
+ class User < ActiveRecord::Base
50
+ include AridCache
51
+ has_many :pets
52
+ has_one :preferences
53
+ named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
54
+ end
55
+
56
+ ==== ARID Cache provides these methods:
57
+
58
+ User.cached_active # caches the user IDs and the count
59
+ User.cached_active_count # gets the count for free
60
+
61
+ user.cached_pets # caches the pets IDs and the count
62
+ user.cached_pets_count # gets the count for free
63
+
64
+ When we call these methods again, instead of doing a full select - usually including
65
+ complex joins or over very large tables which makes this expensive - it just
66
+ selects where the IDs are the cached IDs.
67
+
68
+ It also gives you paging using WillPaginate. The IDs from the cache are paginated and
69
+ only that page is selected from the database - again directly from the table, without
70
+ any complex joins.
71
+
72
+ ==== Some examples of pagination:
73
+
74
+ User.cached_active.paginate(:page => 1, :per_page => 30)
75
+ User.cached_active.paginate(:page => 1)
76
+ User.cached_active.paginate(:page => 3)
77
+
78
+ You can also include options for find, such as <tt>:join</tt>, <tt>:include</tt> and <tt>order</tt>...basically any options that find supports.
79
+
80
+ User.cached_active.paginate(:page => 1, :include => :preferences)
81
+ User.cached_active.paginate(:page => 1, :order => 'created_at DESC') # don't change the order, just enforce it
82
+
83
+ You can limit the results returned using <tt>:limit</tt> and <tt>:offset</tt>:
84
+
85
+ user.cached_pets(:limit => 2, :include => :toys)
86
+ user.cached_pets(:limit => 2, :offset => 3, :include => :toys)
87
+
88
+ ==== You can dynamically create caches
89
+
90
+ User.cached_most_active_users do
91
+ self.active.find(:order => 'activity DESC', :limit => 10)
92
+ end
93
+
94
+ Dynamic caches that make use of other cached collections:
95
+
96
+ @tracks = @genre.cached_highlight_tracks(:order => 'release_date DESC', :include => [:album, :artist]) do
97
+ cached_tracks(:order => 'release_date DESC', :limit => 10, :include => [:album, :artist])
98
+ end
99
+ @artists = @genre.cached_highlight_artists do
100
+ cached_artists(:limit => 10)
101
+ end
102
+ @albums = @genre.cached_highlight_albums(:order => 'release_date DESC', :include => :artist) do
103
+ cached_albums(:order => 'release_date DESC', :limit => 3, :include => :artist)
104
+ end
105
+
106
+ More docs to come...
107
+
108
+ == Note on Patches/Pull Requests
109
+
110
+ * Fork the project.
111
+ * Make your feature addition or bug fix.
112
+ * Add tests for it. This is important so I don't break it in a
113
+ future version unintentionally.
114
+ * Commit, do not mess with rakefile, version, or history.
115
+ (if you want to have your own version, that is fine but
116
+ bump version in a commit by itself I can ignore when I pull)
117
+ * Send me a pull request. Bonus points for topic branches.
118
+
119
+ == Copyright
120
+
121
+ Copyright (c) 2009 Karl Varga. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "arid_cache"
8
+ gem.summary = %Q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
9
+ gem.description = <<-END.gsub(/^\s+/, '')
10
+ ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
11
+
12
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
13
+ END
14
+ gem.email = "kjvarga@gmail.com"
15
+ gem.homepage = "http://github.com/kjvarga/arid_cache"
16
+ gem.authors = ["Karl Varga"]
17
+ gem.add_dependency "will_paginate"
18
+ gem.add_development_dependency "rspec", ">= 1.2.9"
19
+ gem.add_development_dependency "will_paginate"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ require 'spec/rake/spectask'
28
+ Spec::Rake::SpecTask.new(:spec) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
34
+ spec.libs << 'lib' << 'spec'
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :spec => :check_dependencies
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "arid_cache #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.5
@@ -0,0 +1,85 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{arid_cache}
8
+ s.version = "0.0.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Karl Varga"]
12
+ s.date = %q{2009-12-25}
13
+ s.description = %q{ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
14
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
15
+ }
16
+ s.email = %q{kjvarga@gmail.com}
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".gitignore",
23
+ "LICENSE",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "arid_cache.gemspec",
28
+ "init.rb",
29
+ "lib/arid_cache.rb",
30
+ "lib/arid_cache/active_record.rb",
31
+ "lib/arid_cache/cache_proxy.rb",
32
+ "lib/arid_cache/helpers.rb",
33
+ "lib/arid_cache/store.rb",
34
+ "rails/init.rb",
35
+ "spec/arid_cache_spec.rb",
36
+ "spec/spec.opts",
37
+ "spec/spec_helper.rb",
38
+ "tasks/arid_cache_tasks.rake",
39
+ "test/arid_cache_test.rb",
40
+ "test/console",
41
+ "test/db/prepare.rb",
42
+ "test/db/schema.rb",
43
+ "test/fixtures/companies.yml",
44
+ "test/fixtures/users.yml",
45
+ "test/log/.gitignore",
46
+ "test/models/company.rb",
47
+ "test/models/user.rb",
48
+ "test/test_helper.rb"
49
+ ]
50
+ s.homepage = %q{http://github.com/kjvarga/arid_cache}
51
+ s.rdoc_options = ["--charset=UTF-8"]
52
+ s.require_paths = ["lib"]
53
+ s.rubygems_version = %q{1.3.5}
54
+ s.summary = %q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
55
+ s.test_files = [
56
+ "spec/arid_cache_spec.rb",
57
+ "spec/spec_helper.rb",
58
+ "test/arid_cache_test.rb",
59
+ "test/db/prepare.rb",
60
+ "test/db/schema.rb",
61
+ "test/models/company.rb",
62
+ "test/models/user.rb",
63
+ "test/test_helper.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ s.add_runtime_dependency(%q<will_paginate>, [">= 0"])
72
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
73
+ s.add_development_dependency(%q<will_paginate>, [">= 0"])
74
+ else
75
+ s.add_dependency(%q<will_paginate>, [">= 0"])
76
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
77
+ s.add_dependency(%q<will_paginate>, [">= 0"])
78
+ end
79
+ else
80
+ s.add_dependency(%q<will_paginate>, [">= 0"])
81
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
82
+ s.add_dependency(%q<will_paginate>, [">= 0"])
83
+ end
84
+ end
85
+
data/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), 'lib', 'arid_cache') # From here
3
+ rescue LoadError
4
+ require 'arid_cache' # From gem
5
+ end
6
+ AridCache.init_rails
@@ -0,0 +1,67 @@
1
+ module AridCache
2
+ module ActiveRecord
3
+ def self.included(base)
4
+ base.extend MirrorMethods
5
+ base.send :include, MirrorMethods
6
+ base.class_eval do
7
+ alias_method_chain :method_missing, :arid_cache
8
+ end
9
+ class << base
10
+ alias_method_chain :method_missing, :arid_cache
11
+ end
12
+ end
13
+
14
+ module MirrorMethods
15
+ def clear_all_caches
16
+ AridCache.cache.clear_class_caches(self)
17
+ AridCache.cache.clear_instance_caches(self)
18
+ end
19
+
20
+ def clear_class_caches
21
+ AridCache.cache.clear_class_caches(self)
22
+ end
23
+
24
+ def clear_instance_caches
25
+ AridCache.cache.clear_instance_caches(self)
26
+ end
27
+
28
+ def get_singleton
29
+ class << self; self; end
30
+ end
31
+
32
+ # Return a cache key for the given key e.g.
33
+ # User.arid_cache_key('companies') => 'user-companies'
34
+ # User.first.arid_cache_key('companies') => 'users/20090120091123-companies'
35
+ def arid_cache_key(key)
36
+ key = (self.is_a?(Class) ? self.name.downcase : self.cache_key) + '-' + key.to_s
37
+ 'arid-cache-' + key
38
+ end
39
+
40
+ def respond_to?(method, include_private = false) #:nodoc:
41
+ if (method.to_s =~ /^class_cache_.*|cache_.*|cached_.*(_count)?$/).nil?
42
+ super(method, include_private)
43
+ elsif method.to_s =~ /^cached_(.*)_count$/
44
+ AridCache.store.has?(self, "#{$1}_count") || AridCache.store.has?(self, $1) || super("#{$1}_count", include_private) || super($1, include_private)
45
+ elsif method.to_s =~ /^cached_(.*)$/
46
+ AridCache.store.has?(self, $1) || super($1, include_private)
47
+ else
48
+ super(method, include_private)
49
+ end
50
+ end
51
+
52
+ protected
53
+
54
+ # Intercept methods beginning with <tt>cached_</tt>
55
+ def method_missing_with_arid_cache(method, *args, &block) #:nodoc:
56
+ opts = args.empty? ? {} : args.first
57
+ if method.to_s =~ /^cache_(.*)$/
58
+ AridCache.define(self, $1, opts, &block)
59
+ elsif method.to_s =~ /^cached_(.*)$/
60
+ AridCache.lookup(self, $1, opts, &block)
61
+ else
62
+ method_missing_without_arid_cache(method, *args)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,209 @@
1
+ module AridCache
2
+ class CacheProxy
3
+ attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records
4
+
5
+ # AridCache::CacheProxy::Result
6
+ #
7
+ # This struct is stored in the cache and stores information we need
8
+ # to re-query for results.
9
+ Result = Struct.new(:ids, :klass, :count) do
10
+
11
+ def has_count?
12
+ !count.nil?
13
+ end
14
+
15
+ def has_ids?
16
+ !ids.nil?
17
+ end
18
+
19
+ def klass=(value)
20
+ self['klass'] = value.is_a?(Class) ? value.name : value
21
+ end
22
+
23
+ def klass
24
+ self['klass'].constantize unless self['klass'].nil?
25
+ end
26
+ end
27
+
28
+ def self.clear_all_caches
29
+ Rails.cache.delete_matched(%r[arid-cache-.*])
30
+ end
31
+
32
+ def self.clear_class_caches(object)
33
+ key = (object.is_a?(Class) ? object : object.class).name.downcase + '-'
34
+ Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
35
+ end
36
+
37
+ def self.clear_instance_caches(object)
38
+ key = (object.is_a?(Class) ? object : object.class).name.pluralize.downcase
39
+ Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
40
+ end
41
+
42
+ def self.has?(object, key)
43
+ Rails.cache.exist?(object.arid_cache_key(key))
44
+ end
45
+
46
+ def self.fetch_count(object, key, opts, &block)
47
+ CacheProxy.new(object, key, opts, &block).fetch_count
48
+ end
49
+
50
+ def self.fetch(object, key, opts, &block)
51
+ CacheProxy.new(object, key, opts, &block).fetch
52
+ end
53
+
54
+ def initialize(object, key, opts, &block)
55
+ self.object = object
56
+ self.key = key
57
+ self.opts = opts || {}
58
+ self.blueprint = AridCache.store.find(object, key)
59
+ self.cache_key = object.arid_cache_key(key)
60
+ self.cached = Rails.cache.read(cache_key)
61
+ self.block = block
62
+ self.records = nil
63
+ end
64
+
65
+ def fetch_count
66
+ if cached.nil? || opts[:force]
67
+ execute_count
68
+ elsif cached.is_a?(AridCache::CacheProxy::Result)
69
+ cached.has_count? ? cached.count : execute_count
70
+ elsif cached.is_a?(Fixnum)
71
+ cached
72
+ elsif cached.respond_to?(:count)
73
+ cached.count
74
+ else
75
+ cached # what else can we do? return it
76
+ end
77
+ end
78
+
79
+ def fetch
80
+ if cached.nil? || opts[:force]
81
+ execute_find
82
+ elsif cached.is_a?(AridCache::CacheProxy::Result)
83
+ if cached.has_ids? # paginate and fetch here
84
+ klass = find_class_of_results
85
+ if opts.include?(:page)
86
+ klass.paginate(cached.ids, opts_for_paginate)
87
+ else
88
+ klass.find(cached.ids, opts_for_find)
89
+ end
90
+ else
91
+ execute_find
92
+ end
93
+ else
94
+ cached # some base type, return it
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def get_records
101
+ block = block || blueprint.proc
102
+ self.records = block.nil? ? object.instance_eval(key) : object.instance_eval(&block)
103
+ end
104
+
105
+ def execute_find
106
+ get_records
107
+ cached = AridCache::CacheProxy::Result.new
108
+
109
+ if !records.is_a?(Enumerable) || (!records.empty? && !records.first.is_a?(::ActiveRecord::Base))
110
+ cached = records # some base type, cache it as itself
111
+ else
112
+ cached.ids = records.collect(&:id)
113
+ cached.count = records.size
114
+ if records.respond_to?(:proxy_reflection) # association proxy
115
+ cached.klass = records.proxy_reflection.klass
116
+ elsif !records.empty?
117
+ cached.klass = records.first.class
118
+ else
119
+ cached.klass = object_base_class
120
+ end
121
+ end
122
+ Rails.cache.write(cache_key, cached)
123
+
124
+ self.cached = cached
125
+ return_records(records)
126
+ end
127
+
128
+ # Convert records to an array before calling paginate. If we don't do this
129
+ # and the result is a named scope, paginate will trigger an additional query
130
+ # to load the page rather than just using the records we have already fetched.
131
+ #
132
+ # If we are not paginating and the options include :limit (and optionally :offset)
133
+ # apply the limit and offset to the records before returning them.
134
+ #
135
+ # Otherwise we have an issue where all the records are returned the first time
136
+ # the collection is loaded, but on subsequent calls the options_for_find are
137
+ # included and you get different results. Note that with options like :order
138
+ # this cannot be helped. We don't want to modify the query that generates the
139
+ # collection because the idea is to allow getting different perspectives of the
140
+ # cached collection without relying on modifying the collection as a whole.
141
+ #
142
+ # If you do want a specialized, modified, or subset of the collection it's best
143
+ # to define it in a block and have a new cache for it:
144
+ #
145
+ # model.my_special_collection { the_collection(:order => 'new order') }
146
+ def return_records(records)
147
+ if opts.include?(:page)
148
+ records = records.respond_to?(:to_a) ? records.to_a : records
149
+ records.respond_to?(:paginate) ? records.paginate(opts_for_paginate) : records
150
+ elsif opts.include?(:limit)
151
+ records = records.respond_to?(:to_a) ? records.to_a : records
152
+ offset = opts[:offset] || 0
153
+ records[offset, opts[:limit]]
154
+ else
155
+ records
156
+ end
157
+ end
158
+
159
+ def execute_count
160
+ get_records
161
+ cached = AridCache::CacheProxy::Result.new
162
+
163
+ # Just get the count if we can.
164
+ #
165
+ # Because of how AssociationProxy works, if we even look at it, it'll
166
+ # trigger the query. So don't look.
167
+ #
168
+ # Association proxy or named scope. Check for an association first, because
169
+ # it doesn't trigger the select if it's actually named scope. Calling respond_to?
170
+ # on an association proxy will hower trigger a select because it loads up the target
171
+ # and passes the respond_to? on to it.
172
+ if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
173
+ cached.count = records.count # just get the count
174
+ cached.klass = object_base_class
175
+ elsif records.is_a?(Enumerable) && (records.empty? || records.first.is_a?(::ActiveRecord::Base))
176
+ cached.ids = records.collect(&:id) # get everything now that we have it
177
+ cached.count = records.size
178
+ cached.klass = records.empty? ? object_base_class : records.first.class
179
+ else
180
+ cached = records # some base type, cache it as itself
181
+ end
182
+
183
+ Rails.cache.write(cache_key, cached)
184
+ self.cached = cached
185
+ cached.respond_to?(:count) ? cached.count : cached
186
+ end
187
+
188
+ def opts_for_paginate
189
+ paginate_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
190
+ paginate_opts[:total_entries] = cached.count
191
+ paginate_opts
192
+ end
193
+
194
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
195
+
196
+ def opts_for_find
197
+ find_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
198
+ find_opts.delete_if { |k,v| !VALID_FIND_OPTIONS.include?(k) }
199
+ end
200
+
201
+ def object_base_class
202
+ object.is_a?(Class) ? object : object.class
203
+ end
204
+
205
+ def find_class_of_results
206
+ opts[:class] || (blueprint && blueprint.opts[:class]) || cached.klass || object_base_class
207
+ end
208
+ end
209
+ end
@@ -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 object.respond_to?(key)
28
+ define(object, key, opts, &block)
29
+ else
30
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query, please call with a block.")
31
+ end
32
+ object.send("cached_#{key}", opts)
33
+ end
34
+
35
+ # Store the options and optional block for a call to the cache.
36
+ #
37
+ # If no block is provided, create one dynamically.
38
+ #
39
+ # @return an AridCache::Store::Blueprint.
40
+ def define(object, key, opts, fetch_method=:fetch, method_name=nil, &block)
41
+ if block.nil? && !object.respond_to?(key)
42
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create a block for your cache item.")
43
+ end
44
+
45
+ # FIXME: Pass default options to store.add
46
+ # Pass nil in for now until we get the cache_ calls working.
47
+ # This means that the first time you define a dynamic cache
48
+ # (by passing in a block), the options you used are not
49
+ # stored in the blueprint and applied to each subsequent call.
50
+ #
51
+ # Otherwise we have a situation where a :limit passed in to the
52
+ # first call persists when no options are passed in on subsequent calls,
53
+ # but if a different :limit is passed in that limit is applied.
54
+ #
55
+ # I think in this scenario one would expect no limit to be applied
56
+ # if no options are passed in.
57
+ #
58
+ # When the cache_ methods are supported, those options should be
59
+ # remembered and applied to the collection however.
60
+ blueprint = AridCache.store.add(object, key, block, nil)
61
+ method_for_cached(object, key, fetch_method, method_name)
62
+ blueprint
63
+ end
64
+
65
+ private
66
+
67
+ def method_for_cached(object, key, fetch_method=:fetch, method_name=nil)
68
+ method_name = "cached_" + (method_name || key)
69
+ if object.is_a?(Class)
70
+ (class << object; self; end).instance_eval do
71
+ define_method(method_name) do |*args, &block|
72
+ opts = args.empty? ? {} : args.first
73
+ AridCache.cache.send(fetch_method, self, key, opts, &block)
74
+ end
75
+ end
76
+ else
77
+ object.class_eval do
78
+ define_method(method_name) do |*args, &block|
79
+ opts = args.empty? ? {} : args.first
80
+ AridCache.cache.send(fetch_method, self, key, opts, &block)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end