custom_counter_cache 0.2.8 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccba1337c291259711f6a18d6604a72c9be6a8a858342b345dab51f434d05816
4
- data.tar.gz: 97d0e479eb6900a090449243a326a5a3572597219bb119af248dade70adc188b
3
+ metadata.gz: b86370bc27fe59b1bafc3f2160e69843c9650e58b6c9e980010eb5f88fb229cd
4
+ data.tar.gz: 0aeccd374cf039a73f248fa4440f3d472859f73f83b4a0f991b432eaba7bf10e
5
5
  SHA512:
6
- metadata.gz: 6d342ee7e3857b846c66f3183ce2010c0f5c9fc9719060c3d542669fd66ca0266dd2caa77b972013ed01b516bf48799b62ffe32188124900ce8ae74bdb0ef2e9
7
- data.tar.gz: c5b2d8ee7b87e16b04cd62c85d358787126112c8781775a9648aec8f31f5d7f808c363a282a27d6713ec4f6bd452f9a4c4f69998980df95befe84f745d1c3c34
6
+ metadata.gz: f0de231b5e888db121d43d3a37909e37b8e3cb1fd4bf0c58ad0195bda8030f4937a1aba70729716c6c23f3ec1aa48888f68308bb998d6e50fe8bcb71529d9bde
7
+ data.tar.gz: 721794ae2b6cb35349209392f390d42352d3f0fa31a10599b7c13e7dd98e00bdba7c29c96ccfd9033d99e4c508aeab845ec2db9e653bd93f21238fb899892d12
@@ -9,17 +9,21 @@ module CustomCounterCache::Model
9
9
 
10
10
  # counter accessors
11
11
  unless column_names.include?(cache_column.to_s)
12
- has_many :counters, as: :countable, dependent: :destroy
12
+ # Only declare the :counters association once per class -- a model
13
+ # can have multiple virtual (non-column) counter caches, and
14
+ # redeclaring has_many :counters for each one just redefines the
15
+ # same reader/writer methods again, which Ruby warns about under -w.
16
+ has_many :counters, as: :countable, dependent: :delete_all unless reflect_on_association(:counters)
13
17
  define_method "#{cache_column}" do
14
18
  # check if the counter is loaded
15
19
  if counters.loaded? && counter = counters.detect{|c| c.key == cache_column.to_s }
16
20
  counter.value
17
21
  else
18
- counters.find_by_key(cache_column.to_s).try(:value).to_i
22
+ counters.find_by(key: cache_column.to_s).try(:value).to_i
19
23
  end
20
24
  end
21
25
  define_method "#{cache_column}=" do |count|
22
- if ( counter = counters.find_by_key(cache_column.to_s) )
26
+ if ( counter = counters.find_by(key: cache_column.to_s) )
23
27
  counter.update_attribute :value, count.to_i
24
28
  else
25
29
  counters.create key: cache_column.to_s, value: count.to_i
@@ -53,18 +57,22 @@ module CustomCounterCache::Model
53
57
  # define callback
54
58
  define_method method_name do
55
59
  # update old association
56
- rails_5_1_or_newer = ActiveModel.version >= Gem::Version.new('5.1.0')
57
- target_key = reflection.options[:polymorphic] ? "#{association}_type" : foreign_key
58
- target_changed = rails_5_1_or_newer ? send("saved_change_to_#{target_key}?") : send("#{target_key}_changed?")
59
- if target_changed
60
- old_id = rails_5_1_or_newer ? send("#{target_key}_before_last_save") : send("#{target_key}_was")
61
- klass = if reflection.options[:polymorphic]
62
- ( old_id || send("#{association}_type") ).constantize
63
- else
64
- reflection.klass
60
+ if reflection.options[:polymorphic]
61
+ type_key = "#{association}_type"
62
+ id_key = "#{association}_id"
63
+ if send("saved_change_to_#{id_key}?") || send("saved_change_to_#{type_key}?")
64
+ old_type = send("saved_change_to_#{type_key}?") ? send("#{type_key}_before_last_save") : send(type_key)
65
+ old_id = send("saved_change_to_#{id_key}?") ? send("#{id_key}_before_last_save") : send(id_key)
66
+ if ( old_type && old_id && record = old_type.constantize.find_by(id: old_id) )
67
+ record.send("update_#{cache_column}")
68
+ end
65
69
  end
66
- if ( old_id && record = klass.find_by(id: old_id) )
67
- record.send("update_#{cache_column}")
70
+ else
71
+ if send("saved_change_to_#{foreign_key}?")
72
+ old_id = send("#{foreign_key}_before_last_save")
73
+ if ( old_id && record = reflection.klass.find_by(id: old_id) )
74
+ record.send("update_#{cache_column}")
75
+ end
68
76
  end
69
77
  end
70
78
  # update new association
@@ -79,9 +87,10 @@ module CustomCounterCache::Model
79
87
  }
80
88
 
81
89
  # set callbacks
82
- after_create method_name, options unless skip_callback.call(:create, options)
83
- after_update method_name, options unless skip_callback.call(:update, options)
84
- after_destroy method_name, options unless skip_callback.call(:destroy, options)
90
+ callback_opts = options.slice(:if, :unless, :prepend)
91
+ after_create method_name, **callback_opts unless skip_callback.call(:create, options)
92
+ after_update method_name, **callback_opts unless skip_callback.call(:update, options)
93
+ after_destroy method_name, **callback_opts unless skip_callback.call(:destroy, options)
85
94
 
86
95
  rescue StandardError => e
87
96
  # Support Heroku's database-less assets:precompile pre-deploy step:
@@ -1,5 +1,5 @@
1
1
  module CustomCounterCache
2
2
 
3
- VERSION = '0.2.8'
3
+ VERSION = '0.3.1'
4
4
 
5
5
  end
data/test/counter_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
- class CounterTest < MiniTest::Unit::TestCase
3
+ class CounterTest < Minitest::Test
4
4
 
5
5
  def setup
6
6
  @user = User.create
@@ -36,7 +36,7 @@ class CounterTest < MiniTest::Unit::TestCase
36
36
  assert_equal 1, @user.published_count
37
37
  3.times { |i| @user.articles.create(state: 'published') }
38
38
  assert_equal 4, @user.published_count
39
- @user.articles.each {|a| a.update_attributes(state: 'unpublished') }
39
+ @user.articles.each {|a| a.update(state: 'unpublished') }
40
40
  assert_equal 0, @user.published_count
41
41
  end
42
42
 
@@ -48,7 +48,7 @@ class CounterTest < MiniTest::Unit::TestCase
48
48
  assert_equal 1, @article.comments_count
49
49
  3.times { |i| @article.comments.create(state: "published") }
50
50
  assert_equal 4, @article.comments_count
51
- @article.comments.each { |c| c.update_attributes(state: "unpublished") }
51
+ @article.comments.each { |c| c.update(state: 'unpublished') }
52
52
  assert_equal 0, @article.comments_count
53
53
  end
54
54
 
@@ -59,7 +59,7 @@ class CounterTest < MiniTest::Unit::TestCase
59
59
  assert_equal 1, @box.reload.green_balls_count
60
60
  3.times { |i| @box.balls.create(color: 'green') }
61
61
  assert_equal 4, @box.reload.green_balls_count
62
- @box.balls.each {|b| b.update_attributes(color: 'red') }
62
+ @box.balls.each {|b| b.update(color: 'red') }
63
63
  assert_equal 0, @box.reload.green_balls_count
64
64
  end
65
65
 
@@ -95,4 +95,188 @@ class CounterTest < MiniTest::Unit::TestCase
95
95
  assert_equal 1, @box.reload.destroyed_balls_count
96
96
  end
97
97
 
98
+ def test_reassigning_article_to_different_user_updates_both_counters
99
+ @user2 = User.create
100
+ @article = @user.articles.create(state: 'unpublished')
101
+ assert_equal 0, @user.reload.published_count
102
+ assert_equal 0, @user2.reload.published_count
103
+ # Changing both user and state triggers the :if condition (state changed)
104
+ # and the reassignment logic inside the callback updates the old user's counter too
105
+ @article.update(user: @user2, state: 'published')
106
+ assert_equal 0, @user.reload.published_count
107
+ assert_equal 1, @user2.reload.published_count
108
+ end
109
+
110
+ # -- destroy loop regression --
111
+ #
112
+ # A consumer's Counter model might mistakenly add `dependent: :destroy` to
113
+ # its `belongs_to :countable` (see test_helper.rb's Counter class and the
114
+ # README "Note" on this). On a belongs_to, dependent: :destroy means "when
115
+ # I am destroyed, also destroy the record I belong to" -- the opposite of
116
+ # what a counter cache needs. Combined with the has_many :counters this gem
117
+ # defines on the countable side, that mistake used to create a destroy
118
+ # loop: destroying a countable record cascaded into destroying its
119
+ # Counters, and each Counter's (wrongly configured) belongs_to cascaded
120
+ # back into destroying the same countable record again, re-running its
121
+ # full before_destroy chain against already-destroyed associations.
122
+ #
123
+ # has_many :counters now uses dependent: :delete_all, which deletes Counter
124
+ # rows with a single SQL statement and never instantiates them or runs
125
+ # their callbacks -- so a misconfigured Counter#belongs_to (like the one in
126
+ # this test suite's fixtures, deliberately) can no longer cause the loop.
127
+
128
+ def test_destroying_a_countable_record_with_a_dependent_association_does_not_recurse
129
+ @user.articles.create!(state: 'published') # gives the user a real Counter row
130
+ note = UserNote.create!(user: @user)
131
+
132
+ @user.destroy!
133
+
134
+ assert @user.destroyed?
135
+ refute UserNote.exists?(note.id)
136
+ end
137
+
138
+ def test_counters_are_removed_via_a_single_delete_without_instantiating_them
139
+ @user.articles.create!(state: 'published')
140
+ assert_equal 1, Counter.where(countable: @user).count
141
+
142
+ # dependent: :delete_all must never call Counter#destroy -- prove it by
143
+ # making Counter#destroy raise, then destroying the user anyway.
144
+ Counter.define_method(:destroy) { raise 'Counter#destroy should not be called' }
145
+
146
+ @user.destroy!
147
+
148
+ assert_equal 0, Counter.where(countable_type: 'User', countable_id: @user.id).count
149
+ ensure
150
+ Counter.send(:remove_method, :destroy) if Counter.instance_methods(false).include?(:destroy)
151
+ end
152
+
153
+ # -- update_counter_cache: polymorphic reassignment --
154
+ #
155
+ # For a polymorphic association, the old owner must be looked up by its
156
+ # old *type* and old *id* together -- either can change independently
157
+ # (moving to a different record of the same type only changes the id;
158
+ # moving to a different type changes both).
159
+
160
+ def test_reassigning_a_polymorphic_association_updates_both_old_and_new_owner_counters
161
+ article1 = @user.articles.create!(state: 'unpublished')
162
+ article2 = @user.articles.create!(state: 'unpublished')
163
+ comment = article1.comments.create!(state: 'unpublished')
164
+ comment.update!(state: 'published')
165
+ assert_equal 1, article1.reload.comments_count
166
+ assert_equal 0, article2.reload.comments_count
167
+
168
+ # The :if condition only fires the callback on a state change, so toggle
169
+ # state alongside the reassignment -- same pattern as
170
+ # test_reassigning_article_to_different_user_updates_both_counters above.
171
+ comment.update!(commentable: article2, state: 'unpublished')
172
+ comment.update!(state: 'published')
173
+
174
+ assert_equal 0, article1.reload.comments_count
175
+ assert_equal 1, article2.reload.comments_count
176
+ end
177
+
178
+ # -- rescue StandardError / Heroku DATABASE_URL guard --
179
+ #
180
+ # Both class methods wrap their body in a rescue that swallows any
181
+ # StandardError raised while table_exists? can't reach a database (e.g.
182
+ # during `assets:precompile` on Heroku, where there is no DB connection at
183
+ # all), but only when DATABASE_URL matches Heroku's placeholder value.
184
+ # Anywhere else, the error must still propagate.
185
+
186
+ def with_stubbed_table_exists(klass, error)
187
+ klass.define_singleton_method(:table_exists?) { raise error }
188
+ yield
189
+ ensure
190
+ klass.singleton_class.send(:remove_method, :table_exists?)
191
+ end
192
+
193
+ def test_define_counter_cache_reraises_when_database_url_is_not_the_heroku_placeholder
194
+ klass = Class.new(ApplicationRecord) { self.table_name = 'users' }
195
+ with_stubbed_table_exists(klass, 'no database connection') do
196
+ assert_raises(RuntimeError) { klass.define_counter_cache(:whatever) { |r| 0 } }
197
+ end
198
+ end
199
+
200
+ def test_define_counter_cache_swallows_error_when_database_url_is_the_heroku_placeholder
201
+ klass = Class.new(ApplicationRecord) { self.table_name = 'users' }
202
+ with_env('DATABASE_URL', 'postgres://user:pass@127.0.0.1/dbname') do
203
+ with_stubbed_table_exists(klass, 'no database connection') do
204
+ klass.define_counter_cache(:whatever) { |r| 0 } # must not raise
205
+ end
206
+ end
207
+ refute klass.method_defined?(:whatever)
208
+ end
209
+
210
+ def test_update_counter_cache_reraises_when_database_url_is_not_the_heroku_placeholder
211
+ klass = Class.new(ApplicationRecord) { self.table_name = 'articles' }
212
+ with_stubbed_table_exists(klass, 'no database connection') do
213
+ assert_raises(RuntimeError) { klass.update_counter_cache(:user, :whatever) }
214
+ end
215
+ end
216
+
217
+ def test_update_counter_cache_swallows_error_when_database_url_is_the_heroku_placeholder
218
+ klass = Class.new(ApplicationRecord) { self.table_name = 'articles' }
219
+ with_env('DATABASE_URL', 'postgres://user:pass@127.0.0.1/dbname') do
220
+ with_stubbed_table_exists(klass, 'no database connection') do
221
+ klass.update_counter_cache(:user, :whatever) # must not raise
222
+ end
223
+ end
224
+ end
225
+
226
+ def with_env(key, value)
227
+ original = ENV[key]
228
+ ENV[key] = value
229
+ yield
230
+ ensure
231
+ ENV[key] = original
232
+ end
233
+
234
+ # -- table_exists? early return (no error, just a missing table) --
235
+
236
+ def test_define_counter_cache_is_a_no_op_when_the_table_does_not_exist
237
+ klass = Class.new(ApplicationRecord) { self.table_name = 'nonexistent_table_xyz' }
238
+ klass.define_counter_cache(:whatever) { |r| 0 }
239
+ refute klass.method_defined?(:whatever)
240
+ refute klass.method_defined?(:update_whatever)
241
+ end
242
+
243
+ def test_update_counter_cache_is_a_no_op_when_the_table_does_not_exist
244
+ klass = Class.new(ApplicationRecord) { self.table_name = 'nonexistent_table_xyz' }
245
+ klass.update_counter_cache(:user, :whatever)
246
+ refute klass.method_defined?(:callback_user_whatever)
247
+ end
248
+
249
+ # -- update_counter_cache: :unless option --
250
+
251
+ def test_unless_option_skips_the_callback_when_true
252
+ @ball = @box.balls.create(color: 'red')
253
+ assert_equal 1, @box.reload.non_green_balls_count
254
+ @ball.update(color: 'green')
255
+ # :unless suppresses the callback here, so the count is stale (still 1)
256
+ # rather than recomputed to 0 -- proving the callback did not run.
257
+ assert_equal 1, @box.reload.non_green_balls_count
258
+ end
259
+
260
+ def test_unless_option_runs_the_callback_when_false
261
+ @ball = @box.balls.create(color: 'green')
262
+ @ball.update(color: 'red')
263
+ assert_equal 1, @box.reload.non_green_balls_count
264
+ end
265
+
266
+ # -- update_counter_cache: :prepend option --
267
+ #
268
+ # NOTE: this checks that :prepend is forwarded to the after_create callback
269
+ # chain (the gem's own responsibility), not that it changes visible
270
+ # execution order -- that's a separate, Rails-internals quirk: plain
271
+ # sequential after_* callbacks in this Rails version don't necessarily run
272
+ # in chain order just because one was prepended (confirmed with a bare
273
+ # `after_create ..., prepend: true` outside this gem entirely).
274
+
275
+ def test_prepend_option_is_forwarded_to_the_callback_chain
276
+ after_create_filters = Ball._create_callbacks.select { |cb| cb.kind == :after }.map(&:filter)
277
+ assert_operator after_create_filters.index(:callback_box_marker_b_count),
278
+ :<, after_create_filters.index(:callback_box_marker_a_count),
279
+ 'expected the prepend: true callback (marker_b) to be ordered before the non-prepended one (marker_a)'
280
+ end
281
+
98
282
  end
data/test/test_helper.rb CHANGED
@@ -39,24 +39,24 @@ ActiveRecord::Schema.define(version: 1) do
39
39
  t.belongs_to :box
40
40
  t.string :color, default: 'red'
41
41
  end
42
+
43
+ create_table :user_notes do |t|
44
+ t.belongs_to :user
45
+ end
46
+
47
+ create_table :user_note_logs do |t|
48
+ t.belongs_to :user_note
49
+ end
42
50
  end
43
51
 
44
52
  class ApplicationRecord < ActiveRecord::Base
45
53
  self.abstract_class = true
46
54
  include CustomCounterCache::Model
47
- @@rails_5_1_or_newer = ActiveModel.version >= Gem::Version.new('5.1.0')
48
-
49
- def saved_change_to_attribute_compat?(attr)
50
- if @@rails_5_1_or_newer
51
- saved_change_to_attribute?(attr)
52
- else
53
- attribute_changed?(attr)
54
- end
55
- end
56
55
  end
57
56
 
58
57
  class User < ApplicationRecord
59
58
  has_many :articles, dependent: :destroy
59
+ has_one :user_note, dependent: :destroy
60
60
  define_counter_cache :published_count do |user|
61
61
  user.articles.where(articles: { state: 'published' }).count
62
62
  end
@@ -64,7 +64,7 @@ end
64
64
 
65
65
  class Article < ApplicationRecord
66
66
  belongs_to :user
67
- update_counter_cache :user, :published_count, if: Proc.new { |article| article.saved_change_to_attribute_compat?(:state) }
67
+ update_counter_cache :user, :published_count, if: Proc.new { |article| article.saved_change_to_attribute?(:state) }
68
68
  has_many :comments, as: :commentable, dependent: :destroy
69
69
  define_counter_cache :comments_count do |article|
70
70
  article.comments.where(state: "published").count
@@ -73,11 +73,15 @@ end
73
73
 
74
74
  class Comment < ApplicationRecord
75
75
  belongs_to :commentable, polymorphic: true
76
- update_counter_cache :commentable, :comments_count, if: Proc.new { |comment| comment.saved_change_to_attribute_compat?(:state) }
76
+ update_counter_cache :commentable, :comments_count, if: Proc.new { |comment| comment.saved_change_to_attribute?(:state) }
77
77
  end
78
78
 
79
79
  class Counter < ApplicationRecord
80
- belongs_to :countable, polymorphic: true
80
+ # Deliberately mirrors a real-world consumer mistake: adding dependent: :destroy
81
+ # to this belongs_to (see README "Note"). It must NOT cause destroying a
82
+ # countable record to recurse back into destroying itself a second time --
83
+ # see DestroyLoopTest, which is what actually guards against this.
84
+ belongs_to :countable, polymorphic: true, dependent: :destroy
81
85
  end
82
86
 
83
87
  class Box < ApplicationRecord
@@ -91,12 +95,44 @@ class Box < ApplicationRecord
91
95
  define_counter_cache :destroyed_balls_count do |box|
92
96
  box.destroyed_balls_count + 1
93
97
  end
98
+ define_counter_cache :non_green_balls_count do |box|
99
+ box.balls.where.not(color: 'green').count
100
+ end
101
+ define_counter_cache :marker_a_count do |box|
102
+ 0
103
+ end
104
+ define_counter_cache :marker_b_count do |box|
105
+ 0
106
+ end
94
107
  end
95
108
 
96
109
  class Ball < ApplicationRecord
97
110
  belongs_to :box
98
111
  scope :green, lambda { where(color: 'green') }
99
- update_counter_cache :box, :green_balls_count, if: Proc.new { |ball| ball.saved_change_to_attribute_compat?(:color) }
112
+ update_counter_cache :box, :green_balls_count, if: Proc.new { |ball| ball.saved_change_to_attribute?(:color) }
100
113
  update_counter_cache :box, :lifetime_balls_count, except: [:update, :destroy]
101
114
  update_counter_cache :box, :destroyed_balls_count, only: [:destroy]
115
+ update_counter_cache :box, :non_green_balls_count, unless: Proc.new { |ball| ball.color == 'green' }
116
+ update_counter_cache :box, :marker_a_count, only: [:create]
117
+ update_counter_cache :box, :marker_b_count, only: [:create], prepend: true
118
+ end
119
+
120
+ # Stands in for something like the `audited` gem: a has_one association whose
121
+ # own destroy writes a dependent log record from a before_destroy callback
122
+ # (matching how `audited` hooks in -- before_destroy, not after_destroy, so
123
+ # the record is still persisted? while the callback runs). If destroying the
124
+ # owner (User) ever recurses back into destroying itself a second time, this
125
+ # fails the second time around because the UserNote is no longer persisted.
126
+ class UserNote < ApplicationRecord
127
+ belongs_to :user
128
+ has_many :logs, class_name: 'UserNoteLog', foreign_key: :user_note_id, dependent: :destroy
129
+ before_destroy :write_log
130
+
131
+ def write_log
132
+ logs.create!
133
+ end
134
+ end
135
+
136
+ class UserNoteLog < ApplicationRecord
137
+ belongs_to :user_note
102
138
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: custom_counter_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cedric Howe
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-02 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,34 +16,34 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.0'
19
+ version: '7.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '4.0'
29
+ version: '7.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: sqlite3
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 1.3.3
34
- - - "<"
37
+ - - "~>"
35
38
  - !ruby/object:Gem::Version
36
- version: 1.4.0
39
+ version: '2.0'
37
40
  type: :development
38
41
  prerelease: false
39
42
  version_requirements: !ruby/object:Gem::Requirement
40
43
  requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: 1.3.3
44
- - - "<"
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.4.0
46
+ version: '2.0'
47
47
  description: ''
48
48
  email: cedric@howe.net
49
49
  executables: []
@@ -59,7 +59,7 @@ homepage: http://github.com/cedric/custom_counter_cache/
59
59
  licenses:
60
60
  - MIT
61
61
  metadata: {}
62
- post_install_message:
62
+ post_install_message:
63
63
  rdoc_options: []
64
64
  require_paths:
65
65
  - lib
@@ -67,18 +67,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: '0'
70
+ version: '3.0'
71
71
  required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: 1.3.6
76
76
  requirements: []
77
- rubygems_version: 3.0.3
78
- signing_key:
77
+ rubygems_version: 3.5.22
78
+ signing_key:
79
79
  specification_version: 4
80
80
  summary: Custom counter_cache functionality that supports conditions and multiple
81
81
  models.
82
82
  test_files:
83
- - test/test_helper.rb
84
83
  - test/counter_test.rb
84
+ - test/test_helper.rb