kaching 0.0.3
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/.gitignore +10 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +4 -0
- data/README.md +216 -0
- data/Rakefile +6 -0
- data/kaching.gemspec +28 -0
- data/lib/kaching.rb +13 -0
- data/lib/kaching/cache_counter.rb +89 -0
- data/lib/kaching/cache_counter_onmethod.rb +80 -0
- data/lib/kaching/cache_list.rb +194 -0
- data/lib/kaching/model_additions.rb +56 -0
- data/lib/kaching/railtie.rb +9 -0
- data/lib/kaching/storage_providers/memory.rb +42 -0
- data/lib/kaching/storage_providers/redis.rb +26 -0
- data/lib/kaching/version.rb +3 -0
- data/spec/lib/cache_counter_spec.rb +99 -0
- data/spec/lib/cache_list_spec.rb +135 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/support/db/connection.rb +4 -0
- data/spec/support/db/database.yml +6 -0
- data/spec/support/db/models.rb +69 -0
- data/spec/support/db/schema.rb +38 -0
- metadata +157 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
module Kaching
|
2
|
+
module CacheList
|
3
|
+
def cache_list(attribute, options = {})
|
4
|
+
container_class = self
|
5
|
+
|
6
|
+
if block_given?
|
7
|
+
self.cache_counter attribute, options do |item|
|
8
|
+
yield(self)
|
9
|
+
end
|
10
|
+
else
|
11
|
+
self.cache_counter attribute, options
|
12
|
+
end
|
13
|
+
|
14
|
+
options = {
|
15
|
+
method_verb: nil,
|
16
|
+
list_method_name: attribute.to_s,
|
17
|
+
item_key: :item,
|
18
|
+
polymorphic: false,
|
19
|
+
add_alter_methods: true,
|
20
|
+
reset_cache_method_name: "reset_list_cache_#{attribute}!",
|
21
|
+
reset_count_cache_method_name: "reset_count_cache_#{attribute}!"
|
22
|
+
}.merge(options || {})
|
23
|
+
|
24
|
+
if options[:method_verb]
|
25
|
+
options = {
|
26
|
+
add_method_name: "#{options[:method_verb]}!",
|
27
|
+
remove_method_name: "un#{options[:method_verb]}!",
|
28
|
+
exists_method_name: "#{options[:method_verb]}s?",
|
29
|
+
toggle_method_name: "toggle_#{options[:method_verb]}!",
|
30
|
+
}.merge(options)
|
31
|
+
else
|
32
|
+
singularized_attribute = attribute.to_s.singularize
|
33
|
+
options = {
|
34
|
+
add_method_name: "add_#{singularized_attribute}!",
|
35
|
+
remove_method_name: "remove_#{singularized_attribute}!",
|
36
|
+
exists_method_name: "has_#{singularized_attribute}?",
|
37
|
+
toggle_method_name: "toggle_#{singularized_attribute}!",
|
38
|
+
}.merge(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
Kaching.logger.info { "LIST NEW #{self.name} #{options}" }
|
42
|
+
|
43
|
+
list_class = options[:class_name].constantize if options[:class_name]
|
44
|
+
list_class ||= attribute.to_s.singularize.classify.constantize
|
45
|
+
|
46
|
+
item_key_id = "#{options[:item_key]}_id"
|
47
|
+
|
48
|
+
if options[:polymorphic]
|
49
|
+
item_key_type = "#{options[:item_key]}_type"
|
50
|
+
end
|
51
|
+
|
52
|
+
if options[:add_alter_methods]
|
53
|
+
container_class.send :define_method, options[:add_method_name] do |*args|
|
54
|
+
item, values = args
|
55
|
+
Kaching.logger.info { "LIST ADD #{self.class.name}##{self.id} #{item.class.name}##{item.id}" }
|
56
|
+
|
57
|
+
values = values ? values.dup : {}
|
58
|
+
|
59
|
+
values.merge!(options[:item_key] => item)
|
60
|
+
values.merge!(options[:create_options]) if options[:create_options]
|
61
|
+
|
62
|
+
return nil if self.send(options[:exists_method_name], item)
|
63
|
+
|
64
|
+
list_item = list_class.new(values)
|
65
|
+
self.send(options[:list_method_name]) << list_item
|
66
|
+
list_item
|
67
|
+
end
|
68
|
+
|
69
|
+
container_class.send :define_method, options[:remove_method_name] do |item|
|
70
|
+
Kaching.logger.info { "LIST REMOVE #{self.class.name}##{self.id} #{item.class.name}##{item.id}" }
|
71
|
+
|
72
|
+
where = { item_key_id => item.id }
|
73
|
+
|
74
|
+
if options[:polymorphic]
|
75
|
+
where[item_key_type] = item.class.name
|
76
|
+
end
|
77
|
+
|
78
|
+
self.send(options[:list_method_name]).where(where).destroy_all
|
79
|
+
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
container_class.send(:define_method, options[:toggle_method_name]) do |*args|
|
84
|
+
item, state = args
|
85
|
+
|
86
|
+
return unless item
|
87
|
+
|
88
|
+
if state.nil?
|
89
|
+
state = !self.send(options[:exists_method_name], item)
|
90
|
+
end
|
91
|
+
|
92
|
+
if state
|
93
|
+
self.send(options[:add_method_name], item)
|
94
|
+
else
|
95
|
+
self.send(options[:remove_method_name], item)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
container_class.send(:define_method, options[:exists_method_name]) do |item|
|
101
|
+
return false if item == self
|
102
|
+
|
103
|
+
hash_key = self.kaching_key(attribute, :list)
|
104
|
+
|
105
|
+
if Kaching.cache_store.hexists(hash_key, "created")
|
106
|
+
Kaching.logger.info { "USING CREATED #{hash_key}" }
|
107
|
+
Kaching.cache_store.hexists hash_key, item.id.to_s
|
108
|
+
else
|
109
|
+
Kaching.logger.info { "NOT CREATED - CREATING #{hash_key}" }
|
110
|
+
|
111
|
+
# fetch all ids
|
112
|
+
query = block_given? ? yield(self) : self.send(options[:list_method_name])
|
113
|
+
all_ids = container_class.connection.select_values(query.select(item_key_id).to_sql)
|
114
|
+
|
115
|
+
Kaching.logger.info { "FETCH all_ids = #{all_ids.length} | #{all_ids}" }
|
116
|
+
|
117
|
+
no_ids = all_ids.empty?
|
118
|
+
|
119
|
+
ids_with_values_for_hash = all_ids.map { |id|
|
120
|
+
[ id, 1 ]
|
121
|
+
}.flatten
|
122
|
+
|
123
|
+
ids_with_values_for_hash << "created" << 1
|
124
|
+
|
125
|
+
# store them with "created" key. "created" means that hash was created and it's not only empty.
|
126
|
+
Kaching.cache_store.send :hmset, *[hash_key, ids_with_values_for_hash].flatten
|
127
|
+
|
128
|
+
if no_ids
|
129
|
+
false
|
130
|
+
else
|
131
|
+
all_ids.include?(item.id)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
container_class.send :define_method, options[:reset_cache_method_name] do
|
137
|
+
Kaching.cache_store.del(self.kaching_key(attribute, :list))
|
138
|
+
|
139
|
+
self.send(options[:reset_count_cache_method_name]) if self.respond_to?(options[:reset_count_cache_method_name])
|
140
|
+
end
|
141
|
+
|
142
|
+
container_class.send(:after_commit) do
|
143
|
+
if self.destroyed?
|
144
|
+
Kaching.cache_store.del(self.kaching_key(attribute, :list))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
foreign_key = Kaching._extract_foreign_key_from(attribute, options, container_class)
|
149
|
+
|
150
|
+
after_commit_method_name = "after_commit_list_#{attribute}"
|
151
|
+
list_class.send(:define_method, after_commit_method_name) do |action|
|
152
|
+
begin
|
153
|
+
Kaching.logger.info { "LIST #{list_class.name} after_commit foreign_key = #{foreign_key}" }
|
154
|
+
|
155
|
+
belongs_to_item = self.send(foreign_key)
|
156
|
+
|
157
|
+
return unless belongs_to_item
|
158
|
+
|
159
|
+
hash_key = belongs_to_item.kaching_key(attribute, :list)
|
160
|
+
|
161
|
+
# don't bother managing the list because it's not created yet. let the has_item?() build it with all items, and then on creation it'll update the list
|
162
|
+
return unless Kaching.cache_store.hexists(hash_key, "created")
|
163
|
+
|
164
|
+
Kaching.logger.info { " > LIST after_commit #{list_class.name} belongs_to(#{foreign_key}) = #{belongs_to_item.class.name}##{belongs_to_item.id} #{action.inspect}" }
|
165
|
+
|
166
|
+
case action
|
167
|
+
when :create
|
168
|
+
Kaching.logger.info { " > LIST SET #{hash_key} #{self.class.name}##{self.id} type = #{Kaching.cache_store.type(hash_key)}" }
|
169
|
+
|
170
|
+
Kaching.cache_store.hset(hash_key, self.send(item_key_id), 1)
|
171
|
+
when :destroy
|
172
|
+
Kaching.logger.info { " > LIST DEL #{hash_key}" }
|
173
|
+
|
174
|
+
Kaching.cache_store.hdel(hash_key, self.send(item_key_id))
|
175
|
+
end
|
176
|
+
rescue
|
177
|
+
puts $!.message
|
178
|
+
puts $!.backtrace.join("\n")
|
179
|
+
raise $!
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
list_class.send(:after_commit) do
|
184
|
+
action = if self.send(:transaction_include_action?, :create)
|
185
|
+
:create
|
186
|
+
elsif self.destroyed?
|
187
|
+
:destroy
|
188
|
+
end
|
189
|
+
|
190
|
+
self.send(after_commit_method_name, action) if action
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'kaching/cache_counter'
|
3
|
+
require 'kaching/cache_list'
|
4
|
+
|
5
|
+
module Kaching
|
6
|
+
# def self.attributes
|
7
|
+
# @attributes ||= []
|
8
|
+
# end
|
9
|
+
|
10
|
+
def self.cache_store
|
11
|
+
Kaching::StorageProviders.Redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.logger
|
15
|
+
@logger ||= begin
|
16
|
+
logger = Logger.new(ENV['ATTRIBUTE_CACHE_LOG'] ? $stdout : '/dev/null')
|
17
|
+
logger.level = Logger::INFO
|
18
|
+
logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self._extract_foreign_key_from(attribute, options, container_class)
|
23
|
+
foreign_key = options[:foreign_key]
|
24
|
+
|
25
|
+
if !foreign_key
|
26
|
+
reflection = container_class.reflections[attribute]
|
27
|
+
foreign_key = reflection.options[:foreign_key] if reflection
|
28
|
+
end
|
29
|
+
|
30
|
+
foreign_key = if foreign_key
|
31
|
+
foreign_key.gsub(/_id$/, "")
|
32
|
+
else
|
33
|
+
container_class.name.underscore.singularize
|
34
|
+
end
|
35
|
+
|
36
|
+
foreign_key
|
37
|
+
end
|
38
|
+
|
39
|
+
module ModelAdditions
|
40
|
+
module ClassMethods
|
41
|
+
include CacheCounter
|
42
|
+
include CacheList
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods
|
46
|
+
def kaching_key(attribute, type)
|
47
|
+
"Kaching::#{type}::#{self.class.name.underscore.singularize}::#{self.id}::#{attribute}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.included(receiver)
|
52
|
+
receiver.extend ClassMethods
|
53
|
+
receiver.send :include, InstanceMethods
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Kaching
|
2
|
+
module StorageProviders
|
3
|
+
class Memory
|
4
|
+
class << self
|
5
|
+
def data
|
6
|
+
@data ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(key)
|
10
|
+
Kaching.logger.info "CACHE read #{key} = #{data[key]}"
|
11
|
+
data[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(key, value)
|
15
|
+
Kaching.logger.info "CACHE write #{key} = #{value}"
|
16
|
+
data[key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def del(key)
|
20
|
+
data.delete(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch(key, &block)
|
24
|
+
Kaching.logger.info "CACHE fetch #{key} = #{data[key].inspect}"
|
25
|
+
get(key) || set(key, block.call)
|
26
|
+
end
|
27
|
+
|
28
|
+
def incr(key)
|
29
|
+
data[key] ||= 0
|
30
|
+
Kaching.logger.info "CACHE inc #{key} = #{data[key] + 1}"
|
31
|
+
data[key] += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def decr(key)
|
35
|
+
data[key] ||= 0
|
36
|
+
Kaching.logger.info "CACHE dec #{key} = #{data[key] - 1}"
|
37
|
+
data[key] -= 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Kaching
|
4
|
+
module StorageProviders
|
5
|
+
def self.Redis
|
6
|
+
if @redis && !@redis.respond_to?(:fetch)
|
7
|
+
def @redis.fetch(key, &block)
|
8
|
+
value = get(key)
|
9
|
+
|
10
|
+
if !value
|
11
|
+
value = block.call
|
12
|
+
set(key, value)
|
13
|
+
end
|
14
|
+
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@redis
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.Redis=(adapter)
|
23
|
+
@redis = adapter
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Kaching::CacheCounter do
|
4
|
+
let(:user) { User.create }
|
5
|
+
|
6
|
+
it "should create method on user" do
|
7
|
+
user.should respond_to(:articles_count)
|
8
|
+
user.should respond_to(:reset_count_cache_articles!)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return up-to-date count with method" do
|
12
|
+
user.articles_count.should == 0
|
13
|
+
user.articles << Article.new
|
14
|
+
user.articles_count.should == 1
|
15
|
+
user.articles << Article.new
|
16
|
+
user.articles_count.should == 2
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should update count" do
|
20
|
+
user.articles_count.should == 0
|
21
|
+
user.articles << Article.new
|
22
|
+
|
23
|
+
Kaching.cache_store.get(user.kaching_key(:articles, :count)).to_i.should == 1
|
24
|
+
|
25
|
+
user.reset_count_cache_articles!
|
26
|
+
|
27
|
+
Kaching.cache_store.get(user.kaching_key(:articles, :count)).should be_blank
|
28
|
+
|
29
|
+
user.articles_count.should == user.articles_count
|
30
|
+
|
31
|
+
Kaching.cache_store.get(user.kaching_key(:articles, :count)).to_i.should == 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should return up-to-date count for user with items" do
|
35
|
+
user.articles << Article.new
|
36
|
+
user.articles << Article.new
|
37
|
+
user.articles_count.should == 2
|
38
|
+
|
39
|
+
id = user.id
|
40
|
+
|
41
|
+
user = User.find(id)
|
42
|
+
user.articles_count.should == 2
|
43
|
+
|
44
|
+
user.articles << Article.new
|
45
|
+
user.articles_count.should == 3
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should fetch count where not exists yet (_count method wasn't called and inserted new record)" do
|
49
|
+
user.articles << Article.new
|
50
|
+
user.articles << Article.new
|
51
|
+
user.articles_count.should == 2
|
52
|
+
user.reset_count_cache_articles!
|
53
|
+
|
54
|
+
user.articles << Article.new
|
55
|
+
user.articles_count.should == 3
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should use count blocks if passed" do
|
59
|
+
user.following_users_count.should == 0
|
60
|
+
Follow.create(user_id: user.id, item: User.create)
|
61
|
+
user.following_users_count.should == 1
|
62
|
+
Follow.create(user_id: user.id, item: User.create)
|
63
|
+
user.following_users_count.should == 2
|
64
|
+
end
|
65
|
+
|
66
|
+
context "with blocks" do
|
67
|
+
it "should use count blocks if passed and show up-to-date count for existing user" do
|
68
|
+
Follow.create!(user_id: user.id, item: User.create)
|
69
|
+
Follow.create!(user_id: user.id, item: User.create)
|
70
|
+
user.following_users_count.should == 2
|
71
|
+
|
72
|
+
id = user.id
|
73
|
+
|
74
|
+
user = User.find(id)
|
75
|
+
user.following_users_count.should == 2
|
76
|
+
|
77
|
+
Follow.create!(user_id: user.id, item: User.create)
|
78
|
+
user.following_users_count.should == 3
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should use count blocks if passed, other side of association" do
|
82
|
+
follower = User.create
|
83
|
+
|
84
|
+
user.follower_users_count.should == 0
|
85
|
+
|
86
|
+
Follow.create(user_id: follower.id, item: user)
|
87
|
+
user.follower_users_count.should == 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "custom options" do
|
92
|
+
it "should find belongs_to by options" do
|
93
|
+
user.cars_count.should == 0
|
94
|
+
car = Car.create!(driver: user)
|
95
|
+
user.cars.count.should == 1
|
96
|
+
user.cars_count.should == 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Kaching::CacheList do
|
4
|
+
let(:user) { User.create }
|
5
|
+
|
6
|
+
context "non-polymorphic" do
|
7
|
+
it "should add/remove/has" do
|
8
|
+
movie1 = Movie.create!
|
9
|
+
movie2 = Movie.create!
|
10
|
+
|
11
|
+
user.has_user_movie?(movie1).should be_false
|
12
|
+
user.add_user_movie!(movie1)
|
13
|
+
|
14
|
+
user.has_user_movie?(movie1).should be_true
|
15
|
+
|
16
|
+
user.add_user_movie!(movie2)
|
17
|
+
user.has_user_movie?(movie2).should be_true
|
18
|
+
|
19
|
+
user.remove_user_movie!(movie1)
|
20
|
+
user.has_user_movie?(movie1).should be_false
|
21
|
+
user.has_user_movie?(movie2).should be_true
|
22
|
+
|
23
|
+
id = user.id
|
24
|
+
|
25
|
+
user = User.find(id)
|
26
|
+
user.has_user_movie?(movie1).should be_false
|
27
|
+
user.has_user_movie?(movie2).should be_true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "polymorphic" do
|
32
|
+
it "should create method on user" do
|
33
|
+
user.should respond_to(:like!)
|
34
|
+
user.should respond_to(:unlike!)
|
35
|
+
user.should respond_to(:likes?)
|
36
|
+
user.should respond_to(:likes)
|
37
|
+
user.should respond_to(:likes_count)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should add/remove/has" do
|
41
|
+
Movie.create!
|
42
|
+
|
43
|
+
movie1 = Movie.create!
|
44
|
+
movie2 = Movie.create!
|
45
|
+
|
46
|
+
user.likes_count.should == 0
|
47
|
+
user.likes?(movie1).should be_false
|
48
|
+
user.like!(movie1)
|
49
|
+
user.like!(movie2)
|
50
|
+
user.likes_count.should == 2
|
51
|
+
user.likes?(movie1).should be_true
|
52
|
+
user.likes?(movie2).should be_true
|
53
|
+
user.unlike!(movie1)
|
54
|
+
user.likes_count.should == 1
|
55
|
+
user.likes?(movie1).should be_false
|
56
|
+
|
57
|
+
id = user.id
|
58
|
+
|
59
|
+
user = User.find(id)
|
60
|
+
user.likes?(movie1).should be_false
|
61
|
+
user.likes?(movie2).should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should support creation/has w/o built-in methods" do
|
65
|
+
movie1 = Movie.create!
|
66
|
+
movie2 = Movie.create!
|
67
|
+
|
68
|
+
Like.connection.execute "INSERT INTO likes (user_id, item_id, item_type) VALUES (#{user.id}, #{movie1.id}, 'Movie')"
|
69
|
+
Like.create!(user: user, item: movie2)
|
70
|
+
|
71
|
+
user.likes?(movie1).should be_true
|
72
|
+
user.likes?(movie2).should be_true
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should toggle" do
|
76
|
+
movie1 = Movie.create!
|
77
|
+
|
78
|
+
user.likes?(movie1).should be_false
|
79
|
+
user.toggle_like!(movie1)
|
80
|
+
user.likes?(movie1).should be_true
|
81
|
+
|
82
|
+
user.toggle_like!(movie1)
|
83
|
+
user.likes?(movie1).should be_false
|
84
|
+
|
85
|
+
user.like!(movie1)
|
86
|
+
user.likes?(movie1).should be_true
|
87
|
+
|
88
|
+
user.toggle_like!(movie1, false)
|
89
|
+
user.likes?(movie1).should be_false
|
90
|
+
|
91
|
+
user.toggle_like!(movie1, true)
|
92
|
+
user.likes?(movie1).should be_true
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should support creation/has w/o built-in methods, by resetting cache after manual insertion" do
|
96
|
+
movie1 = Movie.create!
|
97
|
+
|
98
|
+
# this creates the cache
|
99
|
+
user.likes?(movie1).should be_false
|
100
|
+
|
101
|
+
# this doesn't update cache
|
102
|
+
Like.connection.execute "INSERT INTO likes (user_id, item_id, item_type) VALUES (#{user.id}, #{movie1.id}, 'Movie')"
|
103
|
+
|
104
|
+
user.reset_list_cache_likes!
|
105
|
+
user.likes?(movie1).should be_true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "two relationships on same model" do
|
110
|
+
it "should manage counts" do
|
111
|
+
follower_user = User.create!
|
112
|
+
followed_user = User.create!
|
113
|
+
|
114
|
+
follower_user.should respond_to(:follower_users_count)
|
115
|
+
follower_user.should respond_to(:following_users_count)
|
116
|
+
|
117
|
+
follower_user.follower_users_count.should == 0
|
118
|
+
followed_user.following_users_count.should == 0
|
119
|
+
|
120
|
+
expect { expect { expect { expect {
|
121
|
+
follower_user.follow! followed_user
|
122
|
+
}.to change(followed_user, :follower_users_count).by(1)
|
123
|
+
}.to change(follower_user, :following_users_count).by(1)
|
124
|
+
}.to_not change(follower_user, :follower_users_count)
|
125
|
+
}.to_not change(followed_user, :following_users_count)
|
126
|
+
|
127
|
+
expect { expect { expect { expect {
|
128
|
+
follower_user.unfollow! followed_user
|
129
|
+
}.to change(followed_user, :follower_users_count).by(-1)
|
130
|
+
}.to change(follower_user, :following_users_count).by(-1)
|
131
|
+
}.to_not change(follower_user, :follower_users_count)
|
132
|
+
}.to_not change(followed_user, :following_users_count)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|