kasket 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
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
+ *.gemspec
21
+
22
+ ## PROJECT::SPECIFIC
23
+ test/debug.log
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Mick Staugaard
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,17 @@
1
+ = kasket
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2009 Mick Staugaard. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "kasket"
8
+ gem.summary = %Q{A write back caching layer on active record}
9
+ gem.description = %Q{A rewrite of cache money}
10
+ gem.email = "mick@staugaard.com"
11
+ gem.homepage = "http://github.com/staugaard/kasket"
12
+ gem.authors = ["Mick Staugaard"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_development_dependency "mocha", ">= 0"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/*_test.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/*_test.rb'
34
+ test.rcov_opts << "--exclude \"test/*,gems/*,/Library/Ruby/*,config/*\" --rails"
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "kasket #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.2
data/lib/kasket.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'active_support'
4
+
5
+ require 'kasket/configuration_mixin'
6
+ require 'kasket/reload_association_mixin'
7
+ require 'kasket/cache'
8
+
9
+ module Kasket
10
+ CONFIGURATION = {:max_collection_size => 100}
11
+
12
+ module_function
13
+
14
+ def cache
15
+ Thread.current["kasket_cache"] ||= Cache.new
16
+ end
17
+
18
+ def setup(options = {})
19
+ CONFIGURATION[:max_collection_size] = options[:max_collection_size] if options[:max_collection_size]
20
+
21
+ ActiveRecord::Base.extend(Kasket::ConfigurationMixin)
22
+ ActiveRecord::Associations::BelongsToAssociation.send(:include, Kasket::ReloadAssociationMixin)
23
+ ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin)
24
+ ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin)
25
+
26
+ #sets up local cache clearing after each request
27
+ begin
28
+ ApplicationController.after_filter do
29
+ Kasket.cache.clear_local
30
+ end
31
+ rescue NameError => e
32
+
33
+ end
34
+
35
+ #sets up local cache clearing after each test case
36
+ begin
37
+ ActiveSupport::TestCase.class_eval do
38
+ setup :clear_cache
39
+ def clear_cache
40
+ Kasket.cache.clear_local
41
+ Rails.cache.clear if Rails.cache.respond_to?(:clear)
42
+ end
43
+ end
44
+ rescue NameError => e
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,82 @@
1
+ module Kasket
2
+ class Cache
3
+ def initialize
4
+ clear_local
5
+ end
6
+
7
+ def read(*args)
8
+ result = @local_cache[args[0]] || Rails.cache.read(*args)
9
+ if result.is_a?(CachedModel)
10
+ result = result.instanciate_model
11
+ elsif result.is_a?(Array)
12
+ models = get_multi(result)
13
+ result = result.map { |key| models[key]}
14
+ end
15
+
16
+ @local_cache[args[0]] = result if result
17
+ result
18
+ end
19
+
20
+ def get_multi(keys)
21
+ map = Hash[keys.zip(keys.map { |key| @local_cache[key] })]
22
+ missing_keys = map.select { |key, value| value.nil? }.map(&:first)
23
+
24
+ unless missing_keys.empty?
25
+ if Rails.cache.respond_to?(:read_multi)
26
+ missing_map = Rails.cache.read_multi(missing_keys)
27
+ missing_map.each do |key, value|
28
+ value = value.instanciate_model if value.is_a?(CachedModel)
29
+ missing_map[key] = @local_cache[key] = value
30
+ end
31
+ map.merge!(missing_map)
32
+ else
33
+ missing_keys.each do |key|
34
+ map[key] = read(key)
35
+ end
36
+ end
37
+ end
38
+
39
+ map
40
+ end
41
+
42
+ def write(*args)
43
+ @local_cache[args[0]] = args[1]
44
+
45
+ if args[1].is_a?(ActiveRecord::Base)
46
+ args[1] = CachedModel.new(args[1])
47
+ end
48
+
49
+ Rails.cache.write(*args)
50
+ end
51
+
52
+ def delete(*args)
53
+ @local_cache.delete(args[0])
54
+ Rails.cache.delete(*args)
55
+ end
56
+
57
+ def delete_local(*keys)
58
+ keys.each do |key|
59
+ @local_cache.delete(key)
60
+ end
61
+ end
62
+
63
+ def delete_matched_local(matcher)
64
+ @local_cache.delete_if { |k,v| k =~ matcher }
65
+ end
66
+
67
+ def clear_local
68
+ @local_cache = {}
69
+ end
70
+
71
+ class CachedModel
72
+ def initialize(model)
73
+ @name = model.class.name
74
+ @attributes = model.instance_variable_get(:@attributes)
75
+ end
76
+
77
+ def instanciate_model
78
+ @name.constantize.send(:instantiate, @attributes)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,79 @@
1
+ module Kasket
2
+ class ConditionsParser
3
+ def initialize(model_class)
4
+ @model_class = model_class
5
+ end
6
+
7
+ def attribute_value_pairs(options)
8
+ #pulls out the conditions from each hash
9
+ condition_fragments = [options[:conditions]]
10
+
11
+ #add the scope to the mix
12
+ if scope = @model_class.send(:scope, :find)
13
+ condition_fragments << scope[:conditions]
14
+ end
15
+
16
+ #add the type if we are on STI
17
+ condition_fragments << {:type => @model_class.name} if @model_class.finder_needs_type_condition?
18
+
19
+ condition_fragments.compact!
20
+
21
+ #parses each conditions fragment but bails if one of the did not parse
22
+ attributes_fragments = condition_fragments.map do |condition_fragment|
23
+ attributes_fragment = attributes_for_conditions(condition_fragment)
24
+ return nil unless attributes_fragment
25
+ attributes_fragment
26
+ end
27
+
28
+ #merges the hashes but bails if there is an overlap
29
+ attributes = attributes_fragments.inject({}) do |memo, attributes_fragment|
30
+ attributes_fragment.each do |attribute, value|
31
+ return nil if memo.has_key?(attribute)
32
+ memo[attribute] = value
33
+ end
34
+ memo
35
+ end
36
+
37
+ attributes.keys.sort.map { |attribute| [attribute.to_sym, attributes[attribute]] }
38
+ end
39
+
40
+ def attributes_for_conditions(conditions)
41
+ pairs = case conditions
42
+ when Hash
43
+ return conditions.stringify_keys
44
+ when String
45
+ parse_indices_from_condition(conditions)
46
+ when Array
47
+ parse_indices_from_condition(*conditions)
48
+ when NilClass
49
+ []
50
+ end
51
+
52
+ return nil unless pairs
53
+
54
+ pairs.inject({}) do |memo, pair|
55
+ return nil if memo.has_key?(pair[0])
56
+ memo[pair[0]] = pair[1]
57
+ memo
58
+ end
59
+ end
60
+
61
+ AND = /\s+AND\s+/i
62
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
63
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
64
+ KEY_EQ_VALUE = /^[\(\s]*#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
65
+
66
+ def parse_indices_from_condition(conditions = '', *values)
67
+ values = values.dup
68
+ conditions.split(AND).inject([]) do |indices, condition|
69
+ matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
70
+ if matched
71
+ value = sql_value == '?' ? values.shift : @model_class.columns_hash[column_name].type_cast(sql_value)
72
+ indices << [column_name, value]
73
+ else
74
+ return nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support'
2
+
3
+ module Kasket
4
+ autoload :ReadMixin, 'kasket/read_mixin'
5
+ autoload :WriteMixin, 'kasket/write_mixin'
6
+ autoload :DirtyMixin, 'kasket/dirty_mixin'
7
+
8
+ module ConfigurationMixin
9
+ def kasket_key_prefix
10
+ @kasket_key_prefix ||= "kasket/#{table_name}/version=#{column_names.join.sum}/"
11
+ end
12
+
13
+ def kasket_key_for(attribute_value_pairs)
14
+ kasket_key_prefix + attribute_value_pairs.map {|attribute, value| attribute.to_s + '=' + value.to_s}.join('/')
15
+ end
16
+
17
+ def kasket_key_for_id(id)
18
+ kasket_key_for([['id', id]])
19
+ end
20
+
21
+ def kasket_indices
22
+ result = @kasket_indices || []
23
+ result += superclass.kasket_indices unless self == ActiveRecord::Base
24
+ result.uniq
25
+ end
26
+
27
+ def has_kasket_index_on?(sorted_attributes)
28
+ kasket_indices.include?(sorted_attributes)
29
+ end
30
+
31
+ def has_kasket(options = {})
32
+ has_kasket_on :id
33
+ end
34
+
35
+ def has_kasket_on(*args)
36
+ attributes = args.sort! { |x, y| x.to_s <=> y.to_s }
37
+ if attributes != [:id] && !kasket_indices.include?([:id])
38
+ has_kasket_on(:id)
39
+ end
40
+
41
+ @kasket_indices ||= []
42
+ @kasket_indices << attributes unless @kasket_indices.include?(attributes)
43
+
44
+ include WriteMixin unless instance_methods.include?('store_in_kasket')
45
+ extend ReadMixin unless methods.include?('without_kasket')
46
+ extend DirtyMixin unless methods.include?('kasket_dirty_methods')
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
1
+ module Kasket
2
+ module DirtyMixin
3
+ def kasket_dirty_methods(*method_names)
4
+ method_names.each do |method|
5
+ unless method_defined?("without_kasket_update_#{method}")
6
+ alias_method("without_kasket_update_#{method}", method)
7
+ define_method(method) do |*args|
8
+ result = send("without_kasket_update_#{method}", *args)
9
+ clear_kasket_indices
10
+ result
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ alias_method :kasket_dirty_method, :kasket_dirty_methods
17
+ end
18
+ end
@@ -0,0 +1,92 @@
1
+ require 'kasket/conditions_parser'
2
+
3
+ module Kasket
4
+ module ReadMixin
5
+
6
+ def self.extended(model_class)
7
+ class << model_class
8
+ alias_method_chain :find_some, :kasket unless methods.include?('find_some_without_kasket')
9
+ alias_method_chain :find_every, :kasket unless methods.include?('find_every_without_kasket')
10
+ end
11
+ end
12
+
13
+ def without_kasket(&block)
14
+ old_value = @use_kasket || true
15
+ @use_kasket = false
16
+ yield
17
+ ensure
18
+ @use_kasket = old_value
19
+ end
20
+
21
+ def find_every_with_kasket(options)
22
+ attribute_value_pairs = kasket_conditions_parser.attribute_value_pairs(options) if cache_safe?(options)
23
+
24
+ limit = (options[:limit] || (scope(:find) || {})[:limit])
25
+
26
+ if (limit.nil? || limit == 1) && attribute_value_pairs && has_kasket_index_on?(attribute_value_pairs.map(&:first))
27
+ collection_key = kasket_key_for(attribute_value_pairs)
28
+ collection_key << '/first' if limit == 1
29
+ unless records = Kasket.cache.read(collection_key)
30
+ records = without_kasket do
31
+ find_every_without_kasket(options)
32
+ end
33
+
34
+ if records.size == 1
35
+ Kasket.cache.write(collection_key, records[0])
36
+ elsif records.size <= Kasket::CONFIGURATION[:max_collection_size]
37
+ records.each { |record| record.store_in_kasket if record }
38
+ Kasket.cache.write(collection_key, records.map(&:kasket_key))
39
+ end
40
+ end
41
+
42
+ Array(records)
43
+ else
44
+ without_kasket do
45
+ find_every_without_kasket(options)
46
+ end
47
+ end
48
+ end
49
+
50
+ def find_some_with_kasket(ids, options)
51
+ attribute_value_pairs = kasket_conditions_parser.attribute_value_pairs(options) if cache_safe?(options)
52
+ attribute_value_pairs << [:id, ids] if attribute_value_pairs
53
+
54
+ limit = (options[:limit] || (scope(:find) || {})[:limit])
55
+
56
+ if limit.nil? && attribute_value_pairs && has_kasket_index_on?(attribute_value_pairs.map(&:first))
57
+ id_to_key_map = Hash[ids.uniq.map { |id| [id, kasket_key_for_id(id)] }]
58
+ cached_record_map = Kasket.cache.get_multi(id_to_key_map.values)
59
+
60
+ missing_keys = Hash[cached_record_map.select { |key, record| record.nil? }].keys
61
+
62
+ return cached_record_map.values if missing_keys.empty?
63
+
64
+ missing_ids = Hash[id_to_key_map.invert.select { |key, id| missing_keys.include?(key) }].values
65
+
66
+ db_records = without_kasket do
67
+ find_some_without_kasket(missing_ids, options)
68
+ end
69
+ db_records.each { |record| record.store_in_kasket if record }
70
+
71
+ (cached_record_map.values + db_records).compact.uniq.sort { |x, y| x.id <=> y.id}
72
+ else
73
+ without_kasket do
74
+ find_some_without_kasket(ids, options)
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def cache_safe?(options)
82
+ @use_kasket != false && [options, scope(:find) || {}].all? do |hash|
83
+ result = hash[:select].nil? && hash[:joins].nil? && hash[:order].nil? && hash[:offset].nil?
84
+ result && (options[:limit].nil? || options[:limit] == 1)
85
+ end
86
+ end
87
+
88
+ def kasket_conditions_parser
89
+ @kasket_conditions_parser ||= Kasket::ConditionsParser.new(self)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,19 @@
1
+ module Kasket
2
+ module ReloadAssociationMixin
3
+ def reload_with_kasket_clearing(*args)
4
+ if loaded?
5
+ clear_local_kasket_indices if respond_to?(:clear_local_kasket_indices)
6
+ end
7
+
8
+ # or maybe something like this?
9
+ #target_class = proxy_reflection.options[:polymorphic] ? association_class : proxy_reflection.klass
10
+ #Kasket.cache.delete_matched_local(/^#{target_class.kasket_key_prefix}/) if target_class.respond_to?(:kasket_key_prefix)
11
+
12
+ reload_without_kasket_clearing(*args)
13
+ end
14
+
15
+ def self.included(base)
16
+ base.alias_method_chain :reload, :kasket_clearing
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ module Kasket
2
+ module WriteMixin
3
+ module ClassMethods
4
+ def remove_from_kasket(ids)
5
+ Array(ids).each do |id|
6
+ Kasket.cache.delete(kasket_key_for_id(id))
7
+ end
8
+ end
9
+
10
+ def update_counters_with_kasket_clearing(*args)
11
+ remove_from_kasket(args[0])
12
+ update_counters_without_kasket_clearing(*args)
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def kasket_key
18
+ @kasket_key ||= new_record? ? nil : self.class.kasket_key_for_id(id)
19
+ end
20
+
21
+ def store_in_kasket
22
+ if !readonly? && kasket_key
23
+ Kasket.cache.write(kasket_key, self)
24
+ end
25
+ end
26
+
27
+ def kasket_keys
28
+ attribute_sets = [attributes.symbolize_keys]
29
+
30
+ if changed?
31
+ old_attributes = Hash[changes.map {|attribute, values| [attribute, values[0]]}].symbolize_keys
32
+ attribute_sets << old_attributes.reverse_merge(attribute_sets[0])
33
+ end
34
+
35
+ keys = []
36
+ self.class.kasket_indices.each do |index|
37
+ keys += attribute_sets.map do |attribute_set|
38
+ self.class.kasket_key_for(index.map { |attribute| [attribute, attribute_set[attribute]]})
39
+ end
40
+ end
41
+
42
+ keys.uniq!
43
+ keys.map! {|key| [key, "#{key}/first"]}
44
+ keys.flatten!
45
+ end
46
+
47
+ def clear_kasket_indices
48
+ kasket_keys.each do |key|
49
+ Kasket.cache.delete(key)
50
+ end
51
+ end
52
+
53
+ def clear_local_kasket_indices
54
+ kasket_keys.each do |key|
55
+ Kasket.cache.delete_local(key)
56
+ end
57
+ end
58
+
59
+ def reload_with_kasket_clearing(*args)
60
+ clear_local_kasket_indices
61
+ reload_without_kasket_clearing(*args)
62
+ end
63
+ end
64
+
65
+ def self.included(model_class)
66
+ model_class.extend ClassMethods
67
+ model_class.send :include, InstanceMethods
68
+
69
+ model_class.after_save :clear_kasket_indices
70
+ model_class.after_destroy :clear_kasket_indices
71
+
72
+ model_class.alias_method_chain :reload, :kasket_clearing
73
+
74
+ class << model_class
75
+ alias_method_chain :update_counters, :kasket_clearing
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,55 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class CacheExpiryTest < ActiveSupport::TestCase
4
+ fixtures :blogs, :posts
5
+
6
+ Post.has_kasket
7
+ Post.has_kasket_on :title
8
+ Post.has_kasket_on :blog_id
9
+
10
+ context "a cached object" do
11
+ setup do
12
+ post = Post.first
13
+ @post = Post.find(post.id)
14
+ assert(Rails.cache.read(@post.kasket_key))
15
+ end
16
+
17
+ should "be removed from cache when deleted" do
18
+ @post.destroy
19
+ assert_nil(Rails.cache.read(@post.kasket_key))
20
+ end
21
+
22
+ should "clear all indices for instance when deleted" do
23
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}")
24
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}/first")
25
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=#{@post.title}")
26
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=#{@post.title}/first")
27
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}")
28
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/first")
29
+ Kasket.cache.expects(:delete).never
30
+
31
+ @post.destroy
32
+ end
33
+
34
+ should "be removed from cache when updated" do
35
+ @post.title = "new title"
36
+ @post.save
37
+ assert_nil(Rails.cache.read(@post.kasket_key))
38
+ end
39
+
40
+ should "clear all indices for instance when updated" do
41
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}")
42
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}/first")
43
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=#{@post.title}")
44
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=#{@post.title}/first")
45
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=new title")
46
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=new title/first")
47
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}")
48
+ Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/first")
49
+ Kasket.cache.expects(:delete).never
50
+
51
+ @post.title = "new title"
52
+ @post.save
53
+ end
54
+ end
55
+ end
data/test/database.yml ADDED
@@ -0,0 +1,7 @@
1
+ test:
2
+ adapter: mysql
3
+ encoding: utf8
4
+ database: kasket_test
5
+ username: root
6
+ password:
7
+ socket: /tmp/mysql.sock
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class DirtyTest < ActiveSupport::TestCase
4
+ fixtures :blogs, :posts
5
+
6
+ Post.has_kasket
7
+ Post.kasket_dirty_methods :make_dirty!
8
+
9
+ should "clear the indices when a dirty method is called" do
10
+ post = Post.first
11
+
12
+ pots = Post.find(post.id)
13
+ assert(Rails.cache.read(post.kasket_key))
14
+
15
+ post.make_dirty!
16
+
17
+ assert_nil(Rails.cache.read(post.kasket_key))
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class FindOneTest < ActiveSupport::TestCase
4
+ fixtures :blogs, :posts
5
+
6
+ Post.has_kasket
7
+
8
+ should "cache find(id) calls" do
9
+ post = Post.first
10
+ assert_nil(Rails.cache.read(post.kasket_key))
11
+ assert_equal(post, Post.find(post.id))
12
+ assert(Rails.cache.read(post.kasket_key))
13
+ Post.connection.expects(:select_all).never
14
+ assert_equal(post, Post.find(post.id))
15
+ end
16
+
17
+ should "not use cache when using the :select option" do
18
+ post = Post.first
19
+ assert_nil(Rails.cache.read(post.kasket_key))
20
+
21
+ Post.find(post.id, :select => 'title')
22
+ assert_nil(Rails.cache.read(post.kasket_key))
23
+
24
+ Post.find(post.id)
25
+ assert(Rails.cache.read(post.kasket_key))
26
+
27
+ Kasket.cache.expects(:read)
28
+ Post.find(post.id, :select => nil)
29
+
30
+ Kasket.cache.expects(:read).never
31
+ Post.find(post.id, :select => 'title')
32
+ end
33
+
34
+ should "respect scope" do
35
+ post = Post.find(Post.first.id)
36
+ other_blog = Blog.first(:conditions => "id != #{post.blog_id}")
37
+
38
+ assert(Rails.cache.read(post.kasket_key))
39
+
40
+ assert_raise(ActiveRecord::RecordNotFound) do
41
+ other_blog.posts.find(post.id)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class FindSomeTest < ActiveSupport::TestCase
4
+ fixtures :blogs, :posts
5
+
6
+ Post.has_kasket
7
+ Post.has_kasket_on :blog_id
8
+
9
+ should "cache find(id, id) calls" do
10
+ post1 = Post.first
11
+ post2 = Post.last
12
+
13
+ assert_nil(Rails.cache.read(post1.kasket_key))
14
+ assert_nil(Rails.cache.read(post2.kasket_key))
15
+
16
+ Post.find(post1.id, post2.id)
17
+
18
+ assert(Rails.cache.read(post1.kasket_key))
19
+ assert(Rails.cache.read(post2.kasket_key))
20
+
21
+ Post.connection.expects(:select_all).never
22
+ Post.find(post1.id, post2.id)
23
+ end
24
+
25
+ should "only lookup the records that are not in the cache" do
26
+ post1 = Post.first
27
+ post2 = Post.last
28
+ assert_equal(post1, Post.find(post1.id))
29
+ assert(Rails.cache.read(post1.kasket_key))
30
+ assert_nil(Rails.cache.read(post2.kasket_key))
31
+
32
+ Post.expects(:find_some_without_kasket).with([post2.id], {}).returns([post2])
33
+ found_posts = Post.find(post1.id, post2.id)
34
+ assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
35
+
36
+ Post.expects(:find_some_without_kasket).never
37
+ found_posts = Post.find(post1.id, post2.id)
38
+ assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
39
+ end
40
+
41
+ should "cache on index other than primary key" do
42
+ blog = blogs(:a_blog)
43
+ posts = Post.find_all_by_blog_id(blog.id)
44
+
45
+ Post.expects(:find_every_without_kasket).never
46
+
47
+ assert_equal(posts, Post.find_all_by_blog_id(blog.id))
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ a_blog:
2
+ name: My Blog
3
+
4
+ other_blog:
5
+ name: Some Other Blog
@@ -0,0 +1,14 @@
1
+ few_comments_1:
2
+ post: has_two_comments
3
+ body: what ever body 1
4
+
5
+ few_comments_2:
6
+ post: has_two_comments
7
+ body: what ever body 2
8
+
9
+ <% (1..10000).each do |i| %>
10
+ many_comments_<%= i %>:
11
+ post: has_many_comments
12
+ body: what ever body <%= i %>
13
+ <% end %>
14
+
@@ -0,0 +1,15 @@
1
+ no_comments:
2
+ blog: a_blog
3
+ title: This post has no comments
4
+
5
+ has_two_comments:
6
+ blog: a_blog
7
+ title: This post has a few comments
8
+
9
+ on_other_blog:
10
+ blog: other_blog
11
+ title: This post has no comments
12
+
13
+ has_many_comments:
14
+ blog: a_blog
15
+ title: A shit load of comments on this one
data/test/helper.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'shoulda'
5
+ require 'active_support'
6
+ require 'active_record'
7
+ require 'active_record/fixtures'
8
+
9
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
10
+ ActiveRecord::Base.establish_connection('test')
11
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
12
+
13
+ load(File.dirname(__FILE__) + "/schema.rb")
14
+
15
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
17
+ require 'kasket'
18
+
19
+ Kasket.setup
20
+
21
+ class ActiveSupport::TestCase
22
+ include ActiveRecord::TestFixtures
23
+
24
+ fixtures :all
25
+
26
+ def create_fixtures(*table_names)
27
+ if block_given?
28
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
29
+ else
30
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
31
+ end
32
+ end
33
+
34
+ self.use_transactional_fixtures = true
35
+
36
+ self.use_instantiated_fixtures = false
37
+ end
38
+
39
+ ActiveSupport::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
40
+ $LOAD_PATH.unshift(ActiveSupport::TestCase.fixture_path)
41
+
42
+ module Rails
43
+ module_function
44
+ CACHE = ActiveSupport::Cache::MemoryStore.new
45
+
46
+ def cache
47
+ CACHE
48
+ end
49
+ end
50
+
51
+ class Comment < ActiveRecord::Base
52
+ belongs_to :post
53
+ end
54
+
55
+ class Post < ActiveRecord::Base
56
+ belongs_to :blog
57
+ has_many :comments
58
+
59
+ def make_dirty!
60
+ self.updated_at = Time.now
61
+ self.connection.execute("UPDATE posts SET updated_at = '#{updated_at.utc.to_s(:db)}' WHERE id = #{id}")
62
+ end
63
+ end
64
+
65
+ class Blog < ActiveRecord::Base
66
+ has_many :posts
67
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,34 @@
1
+ # This file is auto-generated from the current state of the database. Instead of editing this file,
2
+ # please use the migrations feature of Active Record to incrementally modify your database, and
3
+ # then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your database schema. If you need
6
+ # to create the application database on another system, you should be using db:schema:load, not running
7
+ # all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
8
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
9
+ #
10
+ # It's strongly recommended to check this file into your version control system.
11
+
12
+ ActiveRecord::Schema.define(:version => 1) do
13
+
14
+ create_table "comments", :force => true do |t|
15
+ t.text "body"
16
+ t.integer "post_id"
17
+ t.datetime "created_at"
18
+ t.datetime "updated_at"
19
+ end
20
+
21
+ create_table "posts", :force => true do |t|
22
+ t.string "title"
23
+ t.integer "blog_id"
24
+ t.datetime "created_at"
25
+ t.datetime "updated_at"
26
+ end
27
+
28
+ create_table "blogs", :force => true do |t|
29
+ t.string "name"
30
+ t.datetime "created_at"
31
+ t.datetime "updated_at"
32
+ end
33
+
34
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class SerializationTest < ActiveSupport::TestCase
4
+ fixtures :blogs, :posts
5
+
6
+ Post.has_kasket
7
+
8
+ should "store a CachedModel" do
9
+ post = Post.first
10
+ post.store_in_kasket
11
+ assert_instance_of(Kasket::Cache::CachedModel, Rails.cache.read(post.kasket_key))
12
+ end
13
+
14
+ should "bring convert CachedModel to model instances" do
15
+ post = Post.first
16
+ post.store_in_kasket
17
+
18
+ post = Kasket.cache.read(post.kasket_key)
19
+ assert_instance_of(Post, post)
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kasket
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Mick Staugaard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-01 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: mocha
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A rewrite of cache money
36
+ email: mick@staugaard.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/kasket.rb
52
+ - lib/kasket/cache.rb
53
+ - lib/kasket/conditions_parser.rb
54
+ - lib/kasket/configuration_mixin.rb
55
+ - lib/kasket/dirty_mixin.rb
56
+ - lib/kasket/read_mixin.rb
57
+ - lib/kasket/reload_association_mixin.rb
58
+ - lib/kasket/write_mixin.rb
59
+ - test/cache_expiry_test.rb
60
+ - test/database.yml
61
+ - test/dirty_test.rb
62
+ - test/find_one_test.rb
63
+ - test/find_some_test.rb
64
+ - test/fixtures/blogs.yml
65
+ - test/fixtures/comments.yml
66
+ - test/fixtures/posts.yml
67
+ - test/helper.rb
68
+ - test/schema.rb
69
+ - test/serialization_test.rb
70
+ has_rdoc: true
71
+ homepage: http://github.com/staugaard/kasket
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options:
76
+ - --charset=UTF-8
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ version:
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.5
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: A write back caching layer on active record
98
+ test_files:
99
+ - test/cache_expiry_test.rb
100
+ - test/dirty_test.rb
101
+ - test/find_one_test.rb
102
+ - test/find_some_test.rb
103
+ - test/helper.rb
104
+ - test/schema.rb
105
+ - test/serialization_test.rb