counterwise 0.1.5 → 0.1.7
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 +4 -4
- data/README.md +63 -1
- data/app/models/concerns/counter/conditional.rb +1 -0
- data/app/models/concerns/counter/increment.rb +15 -0
- data/app/models/concerns/counter/recalculatable.rb +11 -1
- data/lib/counter/definition.rb +26 -1
- data/lib/counter/integration/countable.rb +5 -2
- data/lib/counter/integration/counters.rb +20 -8
- data/lib/counter/version.rb +1 -1
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 872eddb45910800263c3c79d08192c149f1a93e406b777ace0e37a069b27c581
|
|
4
|
+
data.tar.gz: f1c75031995349a525f45ef0127dbad1dd8e922a31c638936ebecd5da8995b7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acc758026ab1392efd1a18abed79901b51a9b19b3b310e6dcbb71fdf4daec4b06e3c5a191f7232f47a25b1fc402ad054c769daa32d4ea080632cbad5383df939
|
|
7
|
+
data.tar.gz: a81196e23f65e894f2c31780633255fdf2900fe109ea5a7b240d9cd2d54310c79d8a6511886af1208e441b80214e42dcc455b06c1b3a0b7c8cbbd6af2b5de1f9
|
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Counting and aggregation library for Rails.
|
|
|
19
19
|
- [Aggregate a value (e.g. sum of order revenue)](#aggregate-a-value-eg-sum-of-order-revenue)
|
|
20
20
|
- [Hooks](#hooks)
|
|
21
21
|
- [Manual counters](#manual-counters)
|
|
22
|
+
- [Manually calculating a value](#manually-calculating-a-value)
|
|
22
23
|
- [Calculating a value from other counters](#calculating-a-value-from-other-counters)
|
|
23
24
|
- [Defining a conditional counter](#defining-a-conditional-counter)
|
|
24
25
|
- [Testing](#testing)
|
|
@@ -71,6 +72,7 @@ $ rails counter:install:migrations
|
|
|
71
72
|
`Counter::Value` is the value of a counter. So, for example, a User might have many Posts, so a User would have a `counters` association containing a `Counter::Value` for the number of posts. Counters can be accessed via their name `user.posts_counter` or via the `find_counter` method on the association, e.g. `user.counters.find_counter PostCounter`
|
|
72
73
|
|
|
73
74
|
## Basic usage
|
|
75
|
+
|
|
74
76
|
### Define a counter
|
|
75
77
|
|
|
76
78
|
Counters are defined in a seperate class using a small DSL.
|
|
@@ -252,6 +254,34 @@ TotalOrderCounter.counter.value #=> 5
|
|
|
252
254
|
TotalOrderCounter.counter.increment! #=> 6
|
|
253
255
|
```
|
|
254
256
|
|
|
257
|
+
### Manually calculating a value
|
|
258
|
+
|
|
259
|
+
There are edge cases where a counter can't be calculated by associations or other counters. You can tell the counter its value manually, in these cases, by using `calculated_value`. Calculated value takes a lambda and an association. The lambda is called each time an associated record is created, updated, or destroyed.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
class RiskyOrdersCounter < Counter::Definition
|
|
263
|
+
calculated_value ->(customer) { customer.orders.risky.count }, association: :orders
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class Customer
|
|
267
|
+
include Counters::Counter
|
|
268
|
+
counter RiskyOrdersCounter
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
class Order
|
|
272
|
+
include Counter::Changable
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
You may want to change the auto-generated name of the counter value. In this case, you can provide that with `record_name`.
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
class RiskyOrdersCounter < Counter::Definition
|
|
280
|
+
calculated_value ->(customer) { customer.orders.risky.count }, association: :orders
|
|
281
|
+
record_name :risky_customer_orders
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
255
285
|
### Calculating a value from other counters
|
|
256
286
|
|
|
257
287
|
You may also need have a common need to calculate a value from other counters. For example, given counters for the number of purchases and the number of visits, you might want to calculate the conversion rate. You can do this with a `calculate_from` block.
|
|
@@ -326,7 +356,6 @@ We use the `has_changed?` helper to query the ActiveRecord `previous_changes` ha
|
|
|
326
356
|
|
|
327
357
|
Conditional counters work best with a single attribute. If the counter is conditional on e.g. confirmed and subscribed, the update tracking logic becomes very complex especially if the values are both updated at the same time. The solution to this is hopefully Rails generated columns in 7.1 so you can store a "subscribed_and_confirmed" column and check the value of that instead. Rails dirty tracking will need to work with generated columns though; see [this PR](https://github.com/rails/rails/pull/48628).
|
|
328
358
|
|
|
329
|
-
|
|
330
359
|
## Testing
|
|
331
360
|
|
|
332
361
|
### Using Rspec
|
|
@@ -406,9 +435,42 @@ Options:
|
|
|
406
435
|
|
|
407
436
|
---
|
|
408
437
|
|
|
438
|
+
## Release Instructions
|
|
439
|
+
|
|
440
|
+
To release a new version of the counterwise gem:
|
|
441
|
+
|
|
442
|
+
1. **Merge changes**: Ensure all changes are merged into the main branch via pull requests.
|
|
443
|
+
|
|
444
|
+
2. **Update version**: Bump the version number in `lib/counter/version.rb`:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
module Counter
|
|
448
|
+
VERSION = "x.y.z" # Update to new version
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
3. **Commit and push version bump**:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
git add lib/counter/version.rb
|
|
456
|
+
git commit -m "Bump version to x.y.z"
|
|
457
|
+
git push origin main
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
4. **Build and release**:
|
|
461
|
+
```bash
|
|
462
|
+
gem build counter.gemspec
|
|
463
|
+
gem push counterwise-x.y.z.gem
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The gem will be available on [RubyGems.org](https://rubygems.org/gems/counterwise) within a few minutes.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
409
470
|
## TODO
|
|
410
471
|
|
|
411
472
|
See the asociated project in Github but roughly I'm thinking:
|
|
473
|
+
|
|
412
474
|
- Implement the background job pattern for incrementing counters
|
|
413
475
|
- Hierarchical counters. For example, a Site sends many Newsletters and each Newsletter results in many EmailMessages. Each EmailMessage can be marked as spam. How do you create counters for how many spam emails were sent at the Newsletter level and the Site level?
|
|
414
476
|
- Time-based counters for analytics. Instead of a User having one OrderRevenue counter, they would have an OrderRevenue counter for each day. These counters would then be used to produce a chart of their product revenue over the month. Not sure if these are just special counters or something else entirely? Do they use the same ActiveRecord model?
|
|
@@ -19,18 +19,33 @@ module Counter::Increment
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def add_item item
|
|
22
|
+
if definition.calculated_value?
|
|
23
|
+
recalc!
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
return unless increment?(item, :create)
|
|
23
28
|
|
|
24
29
|
increment! by: increment_from_item(item)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def remove_item item
|
|
33
|
+
if definition.calculated_value?
|
|
34
|
+
recalc!
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
return unless decrement?(item, :delete)
|
|
29
39
|
|
|
30
40
|
decrement! by: increment_from_item(item)
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def update_item item
|
|
44
|
+
if definition.calculated_value?
|
|
45
|
+
recalc!
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
if increment?(item, :update)
|
|
35
50
|
increment! by: increment_from_item(item)
|
|
36
51
|
end
|
|
@@ -2,7 +2,9 @@ module Counter::Recalculatable
|
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
4
|
def recalc!
|
|
5
|
-
if definition.
|
|
5
|
+
if definition.calculated_value?
|
|
6
|
+
recalculate_with_value!
|
|
7
|
+
elsif definition.calculated?
|
|
6
8
|
calculate!
|
|
7
9
|
elsif definition.manual?
|
|
8
10
|
raise Counter::Error.new("Can't recalculate a manual counter")
|
|
@@ -26,4 +28,12 @@ module Counter::Recalculatable
|
|
|
26
28
|
def recalc_scope
|
|
27
29
|
parent.association(definition.association_name).scope
|
|
28
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def recalculate_with_value!
|
|
35
|
+
with_lock do
|
|
36
|
+
update!(value: definition.calculated_value.call(parent))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
29
39
|
end
|
data/lib/counter/definition.rb
CHANGED
|
@@ -34,6 +34,10 @@ class Counter::Definition
|
|
|
34
34
|
attr_writer :dependent_counters
|
|
35
35
|
# The block to call to calculate the counter
|
|
36
36
|
attr_accessor :calculated_from
|
|
37
|
+
# The block used to manually set the value
|
|
38
|
+
attr_accessor :calculated_value
|
|
39
|
+
# The counter's record name
|
|
40
|
+
attr_writer :record_name
|
|
37
41
|
|
|
38
42
|
# Is this a counter which sums a column?
|
|
39
43
|
def sum?
|
|
@@ -50,6 +54,11 @@ class Counter::Definition
|
|
|
50
54
|
@conditional
|
|
51
55
|
end
|
|
52
56
|
|
|
57
|
+
# Is this counter using a calculated value?
|
|
58
|
+
def calculated_value?
|
|
59
|
+
@calculated_value.present?
|
|
60
|
+
end
|
|
61
|
+
|
|
53
62
|
# Is this counter calculated from other counters?
|
|
54
63
|
def calculated?
|
|
55
64
|
!@calculated_from.nil?
|
|
@@ -74,11 +83,16 @@ class Counter::Definition
|
|
|
74
83
|
Counter::Value.find_counter self
|
|
75
84
|
end
|
|
76
85
|
|
|
86
|
+
def self.record_name(value)
|
|
87
|
+
instance.record_name = value.to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
77
90
|
# What we record in Counter::Value#name
|
|
78
91
|
def record_name
|
|
92
|
+
return @record_name if @record_name.present?
|
|
79
93
|
return name if global?
|
|
80
94
|
return "#{model.name.underscore}-#{association_name}" if association_name.present?
|
|
81
|
-
|
|
95
|
+
"#{model.name.underscore}-#{name}"
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
def conditions
|
|
@@ -109,6 +123,17 @@ class Counter::Definition
|
|
|
109
123
|
instance.method_name = as.to_s
|
|
110
124
|
end
|
|
111
125
|
|
|
126
|
+
def self.calculated_value(calculation, association: nil)
|
|
127
|
+
instance.association_name = association
|
|
128
|
+
instance.calculated_value = calculation
|
|
129
|
+
set_default_name
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.set_default_name
|
|
133
|
+
instance.name ||= to_s.underscore
|
|
134
|
+
instance.method_name ||= to_s.underscore
|
|
135
|
+
end
|
|
136
|
+
|
|
112
137
|
def self.global
|
|
113
138
|
Counter::Definition.instance.global_counters << instance
|
|
114
139
|
end
|
|
@@ -32,8 +32,11 @@ module Counter::Countable
|
|
|
32
32
|
parent_association.load_target unless parent_association.loaded?
|
|
33
33
|
parent_model = parent_association.target
|
|
34
34
|
next unless parent_model
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
|
|
36
|
+
if parent_model.class.reflect_on_association(:counters) && parent_model.is_a?(counter_definition.model)
|
|
37
|
+
counter = parent_model.counters.find_or_create_counter!(counter_definition)
|
|
38
|
+
yield counter if counter
|
|
39
|
+
end
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -57,13 +57,24 @@ module Counter::Counters
|
|
|
57
57
|
definition = definition_class.instance
|
|
58
58
|
definition.model = self
|
|
59
59
|
|
|
60
|
+
counter_subquery = ->(counter_class) do
|
|
61
|
+
record_name = counter_class.instance.record_name
|
|
62
|
+
|
|
63
|
+
Counter::Value
|
|
64
|
+
.select(:value)
|
|
65
|
+
.where("parent_id = #{table_name}.id AND parent_type = '#{name}' AND name = '#{record_name}'")
|
|
66
|
+
.limit(1)
|
|
67
|
+
.to_sql
|
|
68
|
+
end
|
|
69
|
+
|
|
60
70
|
scope :with_counter_data_from, ->(*counter_classes) {
|
|
61
71
|
subqueries = ["#{table_name}.*"]
|
|
72
|
+
|
|
62
73
|
counter_classes.each do |counter_class|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
subqueries << "(#{sql}) AS #{counter_class.instance.name}_data"
|
|
74
|
+
subquery = counter_subquery.call(counter_class)
|
|
75
|
+
subqueries << Arel.sql("(#{subquery}) AS #{"#{counter_class.instance.name}_data"}")
|
|
66
76
|
end
|
|
77
|
+
|
|
67
78
|
select(subqueries)
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -74,15 +85,16 @@ module Counter::Counters
|
|
|
74
85
|
counter_class.is_a?(Class) &&
|
|
75
86
|
counter_class.ancestors.include?(Counter::Definition)
|
|
76
87
|
}
|
|
77
|
-
|
|
78
|
-
order_hash.map do |counter_class, direction|
|
|
88
|
+
|
|
89
|
+
order_clauses = order_hash.map do |counter_class, direction|
|
|
79
90
|
if counter_class.is_a?(String) || counter_class.is_a?(Symbol)
|
|
80
|
-
|
|
91
|
+
"#{counter_class} #{direction.to_s.upcase}"
|
|
81
92
|
elsif counter_class.ancestors.include?(Counter::Definition)
|
|
82
|
-
|
|
93
|
+
"(#{counter_subquery.call(counter_class)}) #{direction.to_s.upcase}"
|
|
83
94
|
end
|
|
84
95
|
end
|
|
85
|
-
|
|
96
|
+
|
|
97
|
+
with_counter_data_from(*counter_classes).order(Arel.sql(order_clauses.join(", ")))
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
scope :with_counters, -> { includes(:counters) }
|
data/lib/counter/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: counterwise
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamie Lawrence
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '8'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '8'
|
|
27
26
|
description: Counting and aggregation library for Rails.
|
|
28
27
|
email:
|
|
29
28
|
- jamie@ideasasylum.com
|
|
@@ -72,7 +71,6 @@ metadata:
|
|
|
72
71
|
homepage_uri: https://github.com/podia/counter
|
|
73
72
|
source_code_uri: https://github.com/podia/counter
|
|
74
73
|
changelog_uri: https://github.com/podia/counter/CHANGELOG.md
|
|
75
|
-
post_install_message:
|
|
76
74
|
rdoc_options: []
|
|
77
75
|
require_paths:
|
|
78
76
|
- lib
|
|
@@ -80,15 +78,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
80
78
|
requirements:
|
|
81
79
|
- - ">="
|
|
82
80
|
- !ruby/object:Gem::Version
|
|
83
|
-
version:
|
|
81
|
+
version: 3.4.0
|
|
84
82
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
83
|
requirements:
|
|
86
84
|
- - ">="
|
|
87
85
|
- !ruby/object:Gem::Version
|
|
88
86
|
version: '0'
|
|
89
87
|
requirements: []
|
|
90
|
-
rubygems_version: 3.
|
|
91
|
-
signing_key:
|
|
88
|
+
rubygems_version: 3.6.7
|
|
92
89
|
specification_version: 4
|
|
93
90
|
summary: Counters and the counting counters that count them
|
|
94
91
|
test_files: []
|