Skip to content

Commit

Permalink
More explicit code
Browse files Browse the repository at this point in the history
  • Loading branch information
Michaelvilleneuve committed Dec 13, 2023
1 parent 90da4e8 commit 149fe3b
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
statisfy (0.0.7)
statisfy (0.0.8)

GEM
remote: https://rubygems.org/
Expand Down
75 changes: 38 additions & 37 deletions lib/statisfy/counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,12 @@ def count(args = {})
# but the `count` DSL defines them automatically based on the options provided
#
def apply_default_counter_options(args)
define_method(:identifier, args[:uniq_by] || -> { nil })
define_method(:identifier, args[:value] || args[:uniq_by] || -> { nil })
define_method(:scopes, args[:scopes] || Statisfy.configuration.default_scopes || -> { [] })
define_method(:if_async, args[:if_async] || -> { true })
define_method(:decrement?, args[:decrement_if] || -> { false })
define_method(:value, args[:value] || -> {})
define_method(:should_run?, args[:if] || -> { true })
define_method(:on_destroy, args[:on_destroy]) if args[:on_destroy].present?
define_method(:decrement_on_destroy?, args[:decrement_on_destroy].is_a?(Proc) ? args[:decrement_on_destroy] : lambda {
args[:decrement_on_destroy] || true
})
define_method(:decrement_on_destroy?, -> { args[:decrement_on_destroy] != false })
end

#
Expand All @@ -70,6 +66,10 @@ def value(scope: nil, month: nil)
end
end

def aggregate_counter?
const_get(:COUNTER_TYPE) == :aggregate
end

def size(scope: nil, month: nil)
redis_client.scard(key_for(scope:, month:))
end
Expand Down Expand Up @@ -128,7 +128,7 @@ def redis_client
# This allows to run a counter increment manually
# It is useful when you want to backfill counters
#
def initialize_with(resource, options = {})
def trigger_with(resource, options = {})
counter = new
counter.params = resource

Expand Down Expand Up @@ -177,36 +177,22 @@ def month_to_set
end

def process_event
return if destroy_event_handled?
return decrement if can_decrement_on_destroy?
return unless if_async

if value.present?
append(value:)
if self.class.aggregate_counter?
append
else
decrement? ? decrement : increment
end
end

def destroy_event_handled?
return false unless params[:statisfy_trigger] == :destroy && value.blank?

if decrement_on_destroy?
decrement
return true
elsif respond_to?(:on_destroy)
on_destroy
return true
end

false
end

def key_value
value || identifier || params["id"]
def can_decrement_on_destroy?
params[:statisfy_trigger] == :destroy && !self.class.aggregate_counter? && decrement_on_destroy?
end

def custom_key_value?
identifier.present? || value.present?
def value
identifier || params["id"]
end

#
Expand All @@ -223,15 +209,19 @@ def all_counters

def increment
all_counters do |key|
self.class.redis_client.sadd?(key, key_value)
self.class.redis_client.sadd?(uniq_by_ids(key), params["id"]) if custom_key_value?
self.class.redis_client.sadd?(key, value)

# When setting a uniq_by option, we use this set to keep track of the number of unique instances
# with the same identifier.
# When there are no more instances with this identifier, we can decrement the counter
self.class.redis_client.sadd?(key_for_instance_ids(key), params["id"]) if identifier.present?
end
end

#
# To be used to store a list of values instead of a basic counter
#
def append(value:)
def append
all_counters do |key|
self.class.redis_client.rpush(key, value)
end
Expand All @@ -240,18 +230,29 @@ def append(value:)
# rubocop:disable Metrics/AbcSize
def decrement
all_counters do |key|
if custom_key_value?
self.class.redis_client.srem?(uniq_by_ids(key), params["id"])
self.class.redis_client.srem?(key, key_value) if self.class.redis_client.scard(uniq_by_ids(key)).zero?
if identifier.present?
self.class.redis_client.srem?(key_for_instance_ids(key), params["id"])
self.class.redis_client.srem?(key, value) if no_more_instances_with_this_identifier?(key)
else
self.class.redis_client.srem?(key, key_value)
self.class.redis_client.srem?(key, value)
end
end
end
# rubocop:enable Metrics/AbcSize

def uniq_by_ids(key)
"#{key};#{key_value}"
def no_more_instances_with_this_identifier?(key)
self.class.redis_client.scard(key_for_instance_ids(key)).zero?
end

#
# This redis key is used when setting a uniq_by. It stores the list of ids of the main resource (e.g. User)
# in order to count the number of unique instances with the same identifier
#
# When the associated array becomes empty, it means that we can
# decrement the counter because there are no more instances associated with this identifier
#
def key_for_instance_ids(key)
JSON.parse(key).merge("subject_id" => identifier).to_json
end
end
end
Expand Down
Binary file added statisfy-0.0.8.gem
Binary file not shown.
4 changes: 2 additions & 2 deletions statisfy.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Gem::Specification.new do |s|
s.name = "statisfy"
s.version = "0.0.7"
s.version = "0.0.8"
s.required_ruby_version = ">= 3.2.0"
s.date = "2023-12-07"
s.date = "2023-12-13"
s.summary = "A performant and flexible counter solution"
s.description = "A performant and flexible counter solution that allows to make statistics on your models"
s.authors = ["Michaël Villeneuve"]
Expand Down
10 changes: 5 additions & 5 deletions test/statisfy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class NumberOfSteveCounter
assert_equal NumberOfSteveCounter.value, 2
end

test "initialize_with option allows to initialize a counter when a table wasn't empty" do
test "trigger_with option allows to initialize a counter when a table wasn't empty" do
5.times { User.create! }
class UserCreated
include Statisfy::Counter
Expand All @@ -144,13 +144,13 @@ class UserCreated
end

User.find_each do |user|
UserCreated.initialize_with(user)
UserCreated.trigger_with(user)
end

assert_equal UserCreated.value, 5
end

test "initialize_with can skip if validations" do
test "trigger_with can skip if validations" do
3.times { User.create!(name: "Steve") }
2.times { User.create!(name: "Bill") }
Redis.new.flushall
Expand All @@ -163,14 +163,14 @@ class UserCreated
assert_equal UserCreated.value, 0

User.where(name: "Steve").find_each do |user|
UserCreated.initialize_with(user, skip_validation: false)
UserCreated.trigger_with(user, skip_validation: false)
end

# Since previous_changes is missing when initializing, validation fails
assert_equal UserCreated.value, 0

User.where(name: "Steve").find_each do |user|
UserCreated.initialize_with(user, skip_validation: true)
UserCreated.trigger_with(user, skip_validation: true)
end

# Skipping validations allows to initialize the counter
Expand Down

0 comments on commit 149fe3b

Please sign in to comment.