kasket 0.5.2

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