memcacheable 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0260c3ba7467a592bc787a2fb66355703ef1d31f
4
+ data.tar.gz: 093f304c2591ff664e671f03f6972f2cf0c9b4f2
5
+ SHA512:
6
+ metadata.gz: 3dbd55c9b08ad60a26a148c2659c2052cd00806b6d3d9d4e5214922b728418616ae73b7e1e803946b9a457b1c3b1ee230b1f79048d53f5e02260767be6b16ea7
7
+ data.tar.gz: 6cfeed9c6dd105f8aafe8c2b5d30e1b461f27ca35ba84d753350bf65952bb535774be1226b9d4b36c6104730d2dd755c9bb56f584bc34bb690e45cab0a596a2c
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ bundler_stubs/
19
+ .ruby-version
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in memcacheable.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Scott McCormack
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Memcacheable [![Build Status](https://travis-ci.org/flintinatux/memcacheable.png)](https://travis-ci.org/flintinatux/memcacheable) [![Dependency Status](https://gemnasium.com/flintinatux/memcacheable.png)](https://gemnasium.com/flintinatux/memcacheable) [![Code Climate](https://codeclimate.com/github/flintinatux/memcacheable.png)](https://codeclimate.com/github/flintinatux/memcacheable)
2
+
3
+ A Rails concern to make caching ActiveRecord objects as dead-simple as possible. Uses the built-in Rails.cache mechanism, and implements the new finder methods available in Rails 4.0 to ensure maximum future compatibility.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'memcacheable'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install memcacheable
18
+
19
+ ## Usage
20
+
21
+ Let's do some caching!
22
+
23
+ ```ruby
24
+ class Person < ActiveRecord::Base
25
+ include Memcacheable
26
+ end
27
+ ```
28
+
29
+ Boom! Now you can `fetch` a person by their id, like below. When the person gets updated or touched, it will flush the cache, and the person will be reloaded on the next `fetch`.
30
+
31
+ ```ruby
32
+ person = Person.fetch id # caches the person
33
+ person.touch # flushes the cache
34
+ person = Person.fetch id # the cache misses, and the person is reloaded
35
+ ```
36
+
37
+ ### Cache by criteria
38
+
39
+ That's easy-sauce. Time to step up our caching game! _"I want to cache queries by criteria, not just id's!"_ No probs:
40
+
41
+ ```ruby
42
+ class Person < ActiveRecord::Base
43
+ include Memcacheable
44
+ cache_index :name
45
+ cache_index :height, :weight
46
+ end
47
+ ```
48
+
49
+ Powerhouse time! `cache_index` adds these index combinations to the list of cacheable things, so we can fetch single records with `fetch_by`, like this:
50
+
51
+ ```ruby
52
+ person = Person.fetch_by name: 'Scott' # caches an awesome dude
53
+ person.update_attributes name: 'Scottie' # flushes the cache
54
+ person = Person.fetch_by name: 'Scott' # => nil (he's got a new identity!)
55
+
56
+ # You can also do multiple criteria, and order doesn't matter.
57
+ person = Person.fetch_by weight: 175, height: 72
58
+ person.update_attributes height: 71 # he shrunk? oh well, cache flushed
59
+ person = Person.fetch_by weight: 175, height: 71 # fetched and cached with new height
60
+ ```
61
+
62
+ Like noise in your life? Try `fetch_by!` (hard to say: "fetch-by-_bang!_").
63
+
64
+ ```ruby
65
+ person = Person.fetch_by! name: 'Mork' # => ActiveRecord::RecordNotFound
66
+ ```
67
+
68
+ While `fetch_by` just pulls back just one record, you can fetch a collection with `fetch_where`:
69
+
70
+ ```ruby
71
+ people = Person.fetch_where weight: 42, height: 120 # => an array of tall, skinny people
72
+ people.first.update_attributes weight: 43 # one guy gained a little weight --> cache flushed
73
+ people = Person.fetch_where weight: 42, height: 120 # => an array of everyone but that first guy
74
+ ```
75
+
76
+ Trying to `fetch_by` or `fetch_where` by criteria that you didn't specify with `cache_index` will raise an error, because Memcacheable won't know how to bust the cache when things get changed. For example:
77
+
78
+ ```ruby
79
+ Person.fetch_by name: 'Scott' # good
80
+ Person.fetch_by favorite_color: 'red' # shame on you! this wasn't cache_index'd!
81
+ ```
82
+
83
+ **Caveat:** hash-style criteria is currently **required** for `fetch_by` and `fetch_where`. Something like `person.fetch_where 'height < ?', 71` will raise an error.
84
+
85
+ Btw, don't do something stupid like trying to call scope methods on the result of a `fetch_where`. It returns an `Array`, not an `ActiveRecord::Relation`. That means this will blow up on you:
86
+
87
+ ```ruby
88
+ Person.fetch_where(height: 60).limit(5)
89
+ ```
90
+
91
+ I may fix this later, though, because I like scopes.
92
+
93
+ ### Cache associations
94
+
95
+ If you love Rails, then you know you love ActiveRecord associations. Memcacheable loves them too. Check this out:
96
+
97
+ ```ruby
98
+ class Person < ActiveRecord::Base
99
+ has_one :dog
100
+ has_many :kittens
101
+
102
+ include Memcacheable
103
+ cache_has_one :dog
104
+ cache_has_many :kittens
105
+ end
106
+
107
+ class Dog < ActiveRecord::Base
108
+ belongs_to :person, touch: true
109
+
110
+ include Memcacheable
111
+ cache_belongs_to :person
112
+ end
113
+
114
+ class Kitten < ActiveRecord::Base
115
+ belongs_to :person, touch: true
116
+
117
+ include Memcacheable
118
+ cache_index :person_id
119
+ end
120
+ ```
121
+
122
+ **Notice** the `touch: true` above. That's important to bust the parent cache when a child record is updated!
123
+
124
+ So what do we get with all of this caching magic? Why a bunch of dynamic association fetch methods of course! Observe:
125
+
126
+ ```ruby
127
+ dog = person.fetch_dog # his name's Bruiser, btw
128
+ dog.update_attributes name: 'Fido' # flushes the cached association on the person
129
+ dog = person.fetch_dog # finds and caches Fido with his new name
130
+ dog.fetch_person # gets the cached owner
131
+ ```
132
+
133
+ For a slight optimization, specify a `cache_index` on the foreign key of the association, like in the `Kitten` example above. Memcacheable will then do a `fetch_by` or `fetch_where` as appropriate. The cost: two copies in the cache. The gain: when the parent changes but the children don't, the children can be reloaded from the cache. Like this:
134
+
135
+ ```ruby
136
+ person.fetch_kittens # caches the kittens both by criteria and as an association
137
+ person.touch # association cache is flushed, but not the fetch_where
138
+ person.fetch_kittens # reloads the kittens from the cache, and caches as an association
139
+ ```
140
+
141
+ ## Inspiration
142
+
143
+ None of the caching options out there really satisfied my needs, so I wrote this gem. But I was not without inspiration. I learned the basics of Rails caching from the [RailsCasts](http://railscasts.com/) episode on [Model Caching](http://railscasts.com/episodes/115-model-caching-revised), and I borrowed a lot of syntax from the very popular [IdentityCache gem](https://github.com/Shopify/identity_cache) from our friends at [Shopify](http://www.shopify.com/).
144
+
145
+ ## Contributing
146
+
147
+ 1. Fork it
148
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
149
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
150
+ 4. Push to the branch (`git push origin my-new-feature`)
151
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+ task :default => :spec
4
+
5
+ require "bundler/gem_tasks"
@@ -0,0 +1,71 @@
1
+ require 'memcacheable/version'
2
+
3
+ module Memcacheable
4
+ extend ActiveSupport::Autoload
5
+ extend ActiveSupport::Concern
6
+
7
+ autoload :FetchAssociation
8
+ autoload :FetchBelongsTo
9
+ autoload :FetchBy
10
+ autoload :FetchByCriteria
11
+ autoload :FetchHasMany
12
+ autoload :FetchHasOne
13
+ autoload :FetchOne
14
+ autoload :FetchWhere
15
+ autoload :Fetcher
16
+ autoload :Flusher
17
+
18
+ included do
19
+ cattr_accessor :cached_indexes do []; end
20
+ after_commit :flush_cache
21
+ end
22
+
23
+ def flush_cache
24
+ Flusher.new(self).flush
25
+ end
26
+
27
+ def touch
28
+ super
29
+ flush_cache
30
+ end
31
+
32
+ module ClassMethods
33
+ def cache_belongs_to(association)
34
+ define_method "fetch_#{association}" do
35
+ FetchBelongsTo.new(self, association).fetch
36
+ end
37
+ end
38
+
39
+ def cache_has_one(association)
40
+ define_method "fetch_#{association}" do
41
+ FetchHasOne.new(self, association).fetch
42
+ end
43
+ end
44
+
45
+ def cache_has_many(association)
46
+ define_method "fetch_#{association}" do
47
+ FetchHasMany.new(self, association).fetch
48
+ end
49
+ end
50
+
51
+ def cache_index(*fields)
52
+ self.cached_indexes << fields.map(&:to_sym).sort
53
+ end
54
+
55
+ def fetch(id)
56
+ FetchOne.new(self, id).fetch
57
+ end
58
+
59
+ def fetch_by(*args)
60
+ FetchBy.new(self, *args).fetch
61
+ end
62
+
63
+ def fetch_by!(*args)
64
+ fetch_by(*args) || raise(ActiveRecord::RecordNotFound)
65
+ end
66
+
67
+ def fetch_where(*args)
68
+ FetchWhere.new(self, *args).fetch
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,22 @@
1
+ module Memcacheable
2
+ class FetchAssociation < Fetcher
3
+ attr_accessor :object, :association
4
+
5
+ def initialize(object, association)
6
+ self.object = object
7
+ self.association = association
8
+ end
9
+
10
+ def cache_key
11
+ [object, association]
12
+ end
13
+
14
+ def class_name
15
+ object.class.name.downcase
16
+ end
17
+
18
+ def description
19
+ "#{association} for #{class_name} #{object.id}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Memcacheable
2
+ class FetchBelongsTo < FetchAssociation
3
+
4
+ def find_on_cache_miss
5
+ klass = association.to_s.camelize.constantize
6
+ id = object.send "#{association}_id"
7
+ klass.respond_to?(:fetch) ? klass.fetch(id) : klass.find(id) rescue nil
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Memcacheable
2
+ class FetchBy < FetchByCriteria
3
+ def cache_key
4
+ {what: klass.name.downcase}.merge criteria
5
+ end
6
+
7
+ def description
8
+ "#{klass.name.downcase} with #{criteria.inspect}"
9
+ end
10
+
11
+ def find_on_cache_miss
12
+ klass.find_by criteria
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Memcacheable
2
+ class FetchByCriteria < Fetcher
3
+ attr_accessor :klass, :criteria
4
+
5
+ def criteria_cacheable?
6
+ klass.cached_indexes.include? criteria.keys.map(&:to_sym).sort
7
+ end
8
+
9
+ def initialize(klass, *args)
10
+ self.klass = klass
11
+ self.criteria = args.extract_options!
12
+ raise "Only hash-style args accepted! Illegal args: #{args.inspect}" if args.any?
13
+ raise "No cache_index found in #{klass.name} matching fields #{criteria.keys.inspect}!" unless criteria_cacheable?
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memcacheable
2
+ class FetchHasMany < FetchAssociation
3
+ def fetchable?
4
+ klass.respond_to?(:fetch_where) && klass.cached_indexes.include?(["#{class_name}_id".to_sym])
5
+ end
6
+
7
+ def find_on_cache_miss
8
+ criteria = { "#{class_name}_id" => object.id }
9
+ fetchable? ? klass.fetch_where(criteria) : klass.where(criteria).to_a
10
+ end
11
+
12
+ def klass
13
+ @klass ||= association.to_s.classify.constantize
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memcacheable
2
+ class FetchHasOne < FetchAssociation
3
+ def fetchable?
4
+ klass.respond_to?(:fetch_by) && klass.cached_indexes.include?(["#{class_name}_id".to_sym])
5
+ end
6
+
7
+ def find_on_cache_miss
8
+ criteria = { "#{class_name}_id" => object.id }
9
+ fetchable? ? klass.fetch_by(criteria) : klass.find_by(criteria)
10
+ end
11
+
12
+ def klass
13
+ @klass ||= association.to_s.camelize.constantize
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Memcacheable
2
+ class FetchOne < Fetcher
3
+ attr_accessor :klass, :id
4
+
5
+ def initialize(klass, id)
6
+ self.klass = klass
7
+ self.id = id
8
+ end
9
+
10
+ def cache_key
11
+ [klass.name.downcase, id]
12
+ end
13
+
14
+ def description
15
+ "#{klass.name.downcase} #{id}"
16
+ end
17
+
18
+ def find_on_cache_miss
19
+ klass.find id
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Memcacheable
2
+ class FetchWhere < FetchByCriteria
3
+ def cache_key
4
+ {what: klass.name.tableize}.merge criteria
5
+ end
6
+
7
+ def description
8
+ "#{klass.name.tableize} with #{criteria.inspect}"
9
+ end
10
+
11
+ def find_on_cache_miss
12
+ klass.where(criteria).to_a
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Memcacheable
2
+ class Fetcher
3
+ def debug(action)
4
+ Rails.logger.debug "[memcacheable] #{action} #{description}"
5
+ end
6
+
7
+ def fetch
8
+ debug :read
9
+ Rails.cache.fetch cache_key do
10
+ debug :write
11
+ find_on_cache_miss
12
+ end
13
+ end
14
+
15
+ def flush
16
+ Rails.cache.delete cache_key
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module Memcacheable
2
+ class Flusher
3
+ attr_accessor :object
4
+
5
+ OLD_VAL = 0
6
+ NEW_VAL = 1
7
+
8
+ def initialize(object)
9
+ self.object = object
10
+ end
11
+
12
+ def changed_criteria_for(which, fields)
13
+ fields.inject({}) do |hash, field|
14
+ value = object.previous_changes[field][which] rescue object.send(field)
15
+ hash.merge! field => value
16
+ end
17
+ end
18
+
19
+ def flush
20
+ FetchOne.new(object.class, object.id).flush
21
+ object.cached_indexes.each do |fields|
22
+ [OLD_VAL,NEW_VAL].each do |which|
23
+ criteria = changed_criteria_for which, fields
24
+ FetchBy.new(object.class, criteria).flush
25
+ FetchWhere.new(object.class, criteria).flush
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Memcacheable
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'memcacheable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'memcacheable'
8
+ spec.version = Memcacheable::VERSION
9
+ spec.authors = ['Scott McCormack']
10
+ spec.email = ['flintinatux@gmail.com']
11
+ spec.description = %q{A Rails concern to make caching ActiveRecord objects as dead-simple as possible. Uses the built-in Rails.cache mechanism, and implements the new finder methods available in Rails 4.0 to ensure maximum future compatibility.}
12
+ spec.summary = spec.description
13
+ spec.homepage = 'https://github.com/flintinatux/memcacheable'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'activerecord', '>= 4.0.0'
22
+ spec.add_dependency 'activesupport', '>= 4.0.0'
23
+
24
+ spec.add_development_dependency 'activemodel', '>= 4.0.0'
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec', '~> 2.13'
28
+ end
@@ -0,0 +1,272 @@
1
+ require 'spec_helper'
2
+
3
+ class Person < FakeModel
4
+ define_attribute :color, :number, :dog_id
5
+
6
+ include Memcacheable
7
+ cache_index :number
8
+ cache_index :color, :number
9
+ cache_has_one :dog
10
+ cache_has_many :kittens
11
+ end
12
+
13
+ class Dog < FakeModel
14
+ define_attribute :person_id
15
+ include Memcacheable
16
+ end
17
+
18
+ class Kitten < FakeModel
19
+ define_attribute :person_id
20
+ include Memcacheable
21
+ cache_index :person_id
22
+ cache_belongs_to :person
23
+ end
24
+
25
+ describe Memcacheable do
26
+ let(:id) { 123 }
27
+ let(:person) { Person.new id: id, color: 'red', number: 42 }
28
+
29
+ before :all do
30
+ unless defined?(Rails)
31
+ class Rails; cattr_accessor :cache, :logger; end
32
+ Rails.cache = ActiveSupport::Cache::MemoryStore.new
33
+ Rails.logger = ActiveSupport::Logger.new(STDOUT)
34
+ Rails.logger.level = 3
35
+ end
36
+ end
37
+
38
+ after { Rails.cache.clear }
39
+
40
+ describe '::fetch' do
41
+ before do
42
+ Person.stub(:find).with(id).and_return person
43
+ end
44
+
45
+ it "finds the correct record" do
46
+ Person.fetch(id).should eq person
47
+ end
48
+
49
+ it "only queries once and then caches" do
50
+ Person.should_receive(:find).once
51
+ Person.fetch id
52
+ Person.fetch id
53
+ end
54
+ end
55
+
56
+ describe '#touch' do
57
+ before do
58
+ Person.stub(:find).with(id).and_return person
59
+ end
60
+
61
+ it "flushes the cache" do
62
+ Person.fetch(id).touch
63
+ Person.should_receive(:find).once
64
+ Person.fetch id
65
+ end
66
+ end
67
+
68
+ describe '::fetch_by' do
69
+ let(:good_criteria) {{ number: 42, color: 'red' }}
70
+
71
+ before do
72
+ Person.stub(:find_by) do |criteria|
73
+ criteria.all?{ |k,v| person.send(k) == v } ? person : nil
74
+ end
75
+ end
76
+
77
+ it "finds the correct record" do
78
+ Person.fetch_by(good_criteria).should eq person
79
+ end
80
+
81
+ it "only queries once and then caches" do
82
+ Person.should_receive(:find_by).once
83
+ Person.fetch_by good_criteria
84
+ Person.fetch_by good_criteria
85
+ end
86
+
87
+ it "raises on non-hash style args" do
88
+ expect { Person.fetch_by 'color = ?', 'red' }.to raise_error
89
+ end
90
+
91
+ it "raises on non-cached indexes" do
92
+ expect { Person.fetch_by color: 'red' }.to raise_error
93
+ end
94
+
95
+ it "flushes when model updated" do
96
+ person = Person.fetch_by good_criteria
97
+ person.update_attributes number: 7
98
+ Person.should_receive(:find_by).once
99
+ Person.fetch_by good_criteria
100
+ end
101
+ end
102
+
103
+ describe '::fetch_by!' do
104
+ before do
105
+ Person.stub(:find_by).and_return nil
106
+ end
107
+
108
+ it "raises error on nil result" do
109
+ expect { Person.fetch_by!(number: 42) }.to raise_error(ActiveRecord::RecordNotFound)
110
+ end
111
+ end
112
+
113
+ describe 'flushing cached_indexes' do
114
+ let(:old_num) {{ number: 42 }}
115
+ let(:new_num) {{ number: 21 }}
116
+ let(:old_person) {{ person_id: id }}
117
+ let(:new_person) {{ person_id: 345 }}
118
+ let(:kittens) { 3.times.map { Kitten.new person_id: id} }
119
+
120
+ before do
121
+ Person.stub(:find_by) do |criteria|
122
+ criteria.all?{ |k,v| person.send(k) == v } ? person : nil
123
+ end
124
+ Kitten.stub(:where) do |criteria|
125
+ kittens.select do |kitten|
126
+ criteria.all? { |k,v| kitten.send(k) == v }
127
+ end
128
+ end
129
+ end
130
+
131
+ it "flushes old and new values for cached_indexes" do
132
+ Person.fetch_by(old_num).should eq person
133
+ Person.fetch_by(new_num).should eq nil
134
+ person.update_attributes new_num
135
+ Person.fetch_by(old_num).should eq nil
136
+ Person.fetch_by(new_num).should eq person
137
+
138
+ Kitten.fetch_where(old_person).should eq kittens
139
+ Kitten.fetch_where(new_person).should eq []
140
+ kittens.each { |k| k.update_attributes new_person }
141
+ Kitten.fetch_where(old_person).should eq []
142
+ Kitten.fetch_where(new_person).should eq kittens
143
+ end
144
+ end
145
+
146
+ describe '::cache_belongs_to' do
147
+ let(:kitten) { Kitten.new person_id: id }
148
+ before do
149
+ Person.stub(:find).and_return nil
150
+ Person.stub(:find).with(id).and_return person
151
+ end
152
+
153
+ it "defines a new fetch method" do
154
+ kitten.should.respond_to? :fetch_person
155
+ end
156
+
157
+ it "fetches the correct object" do
158
+ kitten.fetch_person.should eq person
159
+ end
160
+
161
+ it "only queries once and then caches" do
162
+ Person.should_receive(:fetch).once
163
+ kitten.fetch_person
164
+ kitten.fetch_person
165
+ end
166
+
167
+ it "flushes when touched by association" do
168
+ kitten.fetch_person
169
+ kitten.touch
170
+ Person.should_receive(:fetch).once
171
+ kitten.fetch_person
172
+ end
173
+ end
174
+
175
+ describe '::cache_has_one' do
176
+ let(:dog) { Dog.new person_id: id }
177
+ before do
178
+ Dog.stub(:find_by) do |criteria|
179
+ criteria.all?{ |k,v| dog.send(k) == v } ? dog : nil
180
+ end
181
+ end
182
+
183
+ it "defines a new fetch method" do
184
+ person.should.respond_to? :fetch_dog
185
+ end
186
+
187
+ it "fetches the correct object" do
188
+ person.fetch_dog.should eq dog
189
+ end
190
+
191
+ it "only queries once and then caches" do
192
+ Dog.should_receive(:find_by).once
193
+ person.fetch_dog
194
+ person.fetch_dog
195
+ end
196
+
197
+ it "flushes when touched by association" do
198
+ person.fetch_dog
199
+ person.touch
200
+ Dog.should_receive(:find_by).once
201
+ person.fetch_dog
202
+ end
203
+ end
204
+
205
+ describe '::fetch_where' do
206
+ let(:kittens) { 3.times.map { Kitten.new person_id: id} }
207
+ before do
208
+ Kitten.stub(:where) do |criteria|
209
+ kittens.select do |kitten|
210
+ criteria.all? { |k,v| kitten.send(k) == v }
211
+ end
212
+ end
213
+ end
214
+
215
+ it "fetches the correct objects" do
216
+ Kitten.fetch_where(person_id: id).should eq kittens
217
+ end
218
+
219
+ it "only queries once and then caches" do
220
+ Kitten.should_receive(:where).once
221
+ Kitten.fetch_where person_id: id
222
+ Kitten.fetch_where person_id: id
223
+ end
224
+
225
+ it "raises on non-hash style args" do
226
+ expect { Kitten.fetch_where 'color = ?', 'red' }.to raise_error
227
+ end
228
+
229
+ it "raises on non-cached indexes" do
230
+ expect { Kitten.fetch_where id: 123 }.to raise_error
231
+ end
232
+
233
+ it "flushes when model updated" do
234
+ kittens = Kitten.fetch_where person_id: id
235
+ kittens.first.update_attributes person_id: 345
236
+ Kitten.should_receive(:where).once
237
+ Kitten.fetch_where person_id: id
238
+ end
239
+ end
240
+
241
+ describe '::cache_has_many' do
242
+ let(:kittens) { 3.times.map { Kitten.new person_id: id} }
243
+ before do
244
+ Kitten.stub(:where) do |criteria|
245
+ kittens.select do |kitten|
246
+ criteria.all? { |k,v| kitten.send(k) == v }
247
+ end
248
+ end
249
+ end
250
+
251
+ it "defines a new fetch method" do
252
+ person.should.respond_to? :fetch_kittens
253
+ end
254
+
255
+ it "fetches the correct collection" do
256
+ person.fetch_kittens.should eq kittens
257
+ end
258
+
259
+ it "only queries once and then caches" do
260
+ Kitten.should_receive(:fetch_where).once
261
+ person.fetch_kittens
262
+ person.fetch_kittens
263
+ end
264
+
265
+ it "flushes when touched by association" do
266
+ person.fetch_kittens
267
+ person.touch
268
+ Kitten.should_receive(:fetch_where).once
269
+ person.fetch_kittens
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_model'
2
+ require 'active_record'
3
+ require 'active_support'
4
+ require 'memcacheable'
5
+
6
+ # Requires supporting ruby files with custom matchers and macros, etc,
7
+ # in spec/support/ and its subdirectories.
8
+ Dir[File.join File.dirname(__FILE__), 'support/**/*.rb'].each {|f| require f}
@@ -0,0 +1,60 @@
1
+ class FakeModel
2
+ include ActiveModel::Dirty
3
+ extend ActiveModel::Callbacks
4
+
5
+ define_model_callbacks :commit
6
+
7
+ def self.define_attribute(*attrs)
8
+ attrs.each do |attr_name|
9
+ define_attribute_method attr_name
10
+ attr_reader attr_name
11
+ define_method "#{attr_name}=" do |value|
12
+ send "#{attr_name}_will_change!" unless value == send(attr_name)
13
+ instance_variable_set "@#{attr_name}", value
14
+ end
15
+ end
16
+ end
17
+
18
+ define_attribute :id, :updated_at
19
+
20
+ def initialize(new_attributes={})
21
+ assign_attributes new_attributes
22
+ self.updated_at = 0
23
+ save # to clear all "changes"
24
+ end
25
+
26
+ def cache_key
27
+ "#{self.class.name.tableize}/#{id}-#{updated_at.to_i}"
28
+ end
29
+
30
+ def commit
31
+ run_callbacks :commit
32
+ end
33
+
34
+ def save
35
+ if changes.any?
36
+ @previously_changed = changes
37
+ @changed_attributes.clear
38
+ commit
39
+ return true
40
+ end
41
+ false
42
+ end
43
+
44
+ def touch
45
+ self.updated_at += 1
46
+ end
47
+
48
+ def update_attributes(new_attributes={})
49
+ assign_attributes new_attributes
50
+ save
51
+ end
52
+
53
+ private
54
+
55
+ def assign_attributes(new_attributes={})
56
+ new_attributes.each do |field, value|
57
+ send "#{field}=", value
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: memcacheable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott McCormack
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 4.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 4.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activemodel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 4.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 4.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '2.13'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '2.13'
97
+ description: A Rails concern to make caching ActiveRecord objects as dead-simple as
98
+ possible. Uses the built-in Rails.cache mechanism, and implements the new finder
99
+ methods available in Rails 4.0 to ensure maximum future compatibility.
100
+ email:
101
+ - flintinatux@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - .gitignore
107
+ - .travis.yml
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - lib/memcacheable.rb
113
+ - lib/memcacheable/fetch_association.rb
114
+ - lib/memcacheable/fetch_belongs_to.rb
115
+ - lib/memcacheable/fetch_by.rb
116
+ - lib/memcacheable/fetch_by_criteria.rb
117
+ - lib/memcacheable/fetch_has_many.rb
118
+ - lib/memcacheable/fetch_has_one.rb
119
+ - lib/memcacheable/fetch_one.rb
120
+ - lib/memcacheable/fetch_where.rb
121
+ - lib/memcacheable/fetcher.rb
122
+ - lib/memcacheable/flusher.rb
123
+ - lib/memcacheable/version.rb
124
+ - memcacheable.gemspec
125
+ - spec/lib/memcacheable_spec.rb
126
+ - spec/spec_helper.rb
127
+ - spec/support/fake_model.rb
128
+ homepage: https://github.com/flintinatux/memcacheable
129
+ licenses:
130
+ - MIT
131
+ metadata: {}
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubyforge_project:
148
+ rubygems_version: 2.0.0.rc.2
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: A Rails concern to make caching ActiveRecord objects as dead-simple as possible.
152
+ Uses the built-in Rails.cache mechanism, and implements the new finder methods available
153
+ in Rails 4.0 to ensure maximum future compatibility.
154
+ test_files:
155
+ - spec/lib/memcacheable_spec.rb
156
+ - spec/spec_helper.rb
157
+ - spec/support/fake_model.rb