kaching 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|