redis-objects-daily-counter 0.2.0

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/README.md ADDED
@@ -0,0 +1,122 @@
1
+ [![CircleCI](https://circleci.com/gh/ryz310/redis-objects-daily-counter.svg?style=svg)](https://circleci.com/gh/ryz310/redis-objects-daily-counter) [![Gem Version](https://badge.fury.io/rb/redis-objects-daily-counter.svg)](https://badge.fury.io/rb/redis-objects-daily-counter) [![Maintainability](https://api.codeclimate.com/v1/badges/3639d1776e23031b1b31/maintainability)](https://codeclimate.com/github/ryz310/redis-objects-daily-counter/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/3639d1776e23031b1b31/test_coverage)](https://codeclimate.com/github/ryz310/redis-objects-daily-counter/test_coverage) [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=ryz310/redis-objects-daily-counter)](https://dependabot.com)
2
+
3
+ # Redis::Objects::Daily::Counter
4
+
5
+ This is a gem which extends [Redis::Objects](https://github.com/nateware/redis-objects) gem. Once install this gem, you can use the daily counter, etc. in addition to the standard features of Redis::Objects. These counters are useful for measuring conversions, implementing API rate limiting, and more.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'redis-objects-daily-counter'
13
+ ```
14
+
15
+ If you want to know about installation and standard usage, please see Redis::Objects' GitHub page.
16
+
17
+ ## Usage
18
+
19
+ `daily_counter` automatically creates keys that are unique to each object, in the format:
20
+
21
+ ```
22
+ model_name:id:field_name:yyyy-mm-dd
23
+ ```
24
+
25
+ For illustration purposes, consider this stub class:
26
+
27
+ ```rb
28
+ class Homepage
29
+ include Redis::Objects
30
+
31
+ daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month
32
+
33
+ def id
34
+ 1
35
+ end
36
+ end
37
+
38
+ # 2021-04-01
39
+ homepage = Homepage.new
40
+ homepage.id # 1
41
+
42
+ homepage.pv.increment
43
+ homepage.pv.increment
44
+ homepage.pv.increment
45
+ puts homepage.pv.value # 3
46
+
47
+ # 2021-04-02 (next day)
48
+ puts homepage.pv.value # 0
49
+ homepage.pv.increment
50
+ homepage.pv.increment
51
+ puts homepage.pv.value # 2
52
+
53
+ start_date = Date.new(2021, 4, 1)
54
+ end_date = Date.new(2021, 4, 2)
55
+ homepage.pv.range(start_date, end_date) # [3, 2]
56
+ ```
57
+
58
+ The daily counter automatically switches the save destination when the date changes.
59
+ You can access past dates counted values like Ruby arrays:
60
+
61
+ ```rb
62
+ # 2021-04-01
63
+ homepage.pv.increment(3)
64
+
65
+ # 2021-04-02 (next day)
66
+ homepage.pv.increment(2)
67
+
68
+ # 2021-04-03 (next day)
69
+ homepage.pv.increment(5)
70
+
71
+ homepage.pv[Date.new(2021, 4, 1)] # => 3
72
+ homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5]
73
+ homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2]
74
+
75
+ homepage.pv.delete(Date.new(2021, 4, 1))
76
+ homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5]
77
+ homepage.pv.at(Date.new(2021, 4, 2)) # => 2
78
+ ```
79
+
80
+ ### Counters
81
+
82
+ I recommend using with `expireat` option.
83
+
84
+ * `annual_counter`
85
+ * Key format: `model_name:id:field_name:yyyy`
86
+ * Redis is a highly volatile key-value store, so I don't recommend using it.
87
+ * `monthly_counter`
88
+ * Key format: `model_name:id:field_name:yyyy-mm`
89
+ * `weekly_counter`
90
+ * Key format: `model_name:id:field_name:yyyyWw`
91
+ * `daily_counter`
92
+ * Key format: `model_name:id:field_name:yyyy-mm-dd`
93
+ * `hourly_counter`
94
+ * Key format: `model_name:id:field_name:yyyy-mm-ddThh`
95
+ * `minutely_counter`
96
+ * Key format: `model_name:id:field_name:yyyy-mm-ddThh:mi`
97
+
98
+ ### Timezone
99
+
100
+ This gem follows Ruby process' time zone, but if you extends Time class by ActiveSupport (e.g. `Time.current`), follows Rails process' timezone.
101
+
102
+ ## Development
103
+
104
+ The development environment for this gem is configured with docker-compose.
105
+ Please use the following command:
106
+
107
+ $ docker-compose up -d
108
+ $ docker-compose run --rm ruby bundle
109
+ $ docker-compose run --rm ruby rspec .
110
+ $ docker-compose run --rm ruby rubocop -a
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/redis-objects-daily-counter. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/redis-objects-daily-counter/blob/master/CODE_OF_CONDUCT.md).
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
119
+
120
+ ## Code of Conduct
121
+
122
+ Everyone interacting in the Redis::Objects::Daily::Counter project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/redis-objects-daily-counter/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'redis-objects-daily-counter'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,17 @@
1
+ version: '3'
2
+
3
+ services:
4
+ ruby:
5
+ build: .
6
+ depends_on:
7
+ - redis
8
+ volumes:
9
+ - .:/my_app
10
+ - bundle:/usr/local/bundle
11
+ redis:
12
+ image: redis:latest
13
+ ports:
14
+ - 6379:6379
15
+
16
+ volumes:
17
+ bundle:
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class AnnualCounter < BaseCounterObject
7
+ private
8
+
9
+ def redis_daily_field_key(date_or_time)
10
+ date_key = date_or_time.strftime('%Y')
11
+ [original_key, date_key].flatten.join(':')
12
+ end
13
+
14
+ def next_key(date, length)
15
+ date.next_year(length - 1)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class BaseCounterObject < Counter
5
+ def initialize(key, *args)
6
+ @original_key = key
7
+ super(redis_daily_field_key(current_time), *args)
8
+ end
9
+
10
+ attr_reader :original_key
11
+
12
+ def [](date_or_time, length = nil)
13
+ if date_or_time.is_a? Range
14
+ range(date_or_time.first, date_or_time.max)
15
+ elsif length
16
+ case length <=> 0
17
+ when 1 then range(date_or_time, next_key(date_or_time, length))
18
+ when 0 then []
19
+ when -1 then nil # Ruby does this (a bit weird)
20
+ end
21
+ else
22
+ at(date_or_time)
23
+ end
24
+ end
25
+ alias slice []
26
+
27
+ def delete(date_or_time)
28
+ redis.del(redis_daily_field_key(date_or_time))
29
+ end
30
+
31
+ def range(start_date, end_date)
32
+ keys = (start_date..end_date).map { |date| redis_daily_field_key(date) }.uniq
33
+ redis.mget(*keys).map(&:to_i)
34
+ end
35
+
36
+ def at(date_or_time)
37
+ redis.get(redis_daily_field_key(date_or_time)).to_i
38
+ end
39
+
40
+ def current_time
41
+ @current_time ||= Time.respond_to?(:current) ? Time.current : Time.now
42
+ end
43
+
44
+ private
45
+
46
+ def redis_daily_field_key(_date_or_time)
47
+ raise 'not implemented'
48
+ end
49
+
50
+ def next_key(_date, _length)
51
+ raise 'not implemented'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class DailyCounter < BaseCounterObject
7
+ private
8
+
9
+ def redis_daily_field_key(date_or_time)
10
+ date_key = date_or_time.strftime('%Y-%m-%d')
11
+ [original_key, date_key].flatten.join(':')
12
+ end
13
+
14
+ def next_key(date, length)
15
+ date + length - 1
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class HourlyCounter < BaseCounterObject
7
+ def range(start_time, end_time)
8
+ keys =
9
+ (start_time.to_i..end_time.to_i)
10
+ .step(3600)
11
+ .map { |integer| redis_daily_field_key(Time.at(integer)) }
12
+ redis.mget(*keys).map(&:to_i)
13
+ end
14
+
15
+ private
16
+
17
+ def redis_daily_field_key(time)
18
+ time_key = time.strftime('%Y-%m-%dT%H')
19
+ [original_key, time_key].flatten.join(':')
20
+ end
21
+
22
+ def next_key(time, length)
23
+ time + 3600 * (length - 1)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class MinutelyCounter < BaseCounterObject
7
+ def range(start_time, end_time)
8
+ keys =
9
+ (start_time.to_i..end_time.to_i)
10
+ .step(60)
11
+ .map { |integer| redis_daily_field_key(Time.at(integer)) }
12
+ redis.mget(*keys).map(&:to_i)
13
+ end
14
+
15
+ private
16
+
17
+ def redis_daily_field_key(time)
18
+ time_key = time.strftime('%Y-%m-%dT%H:%M')
19
+ [original_key, time_key].flatten.join(':')
20
+ end
21
+
22
+ def next_key(time, length)
23
+ time + 60 * (length - 1)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class MonthlyCounter < BaseCounterObject
7
+ private
8
+
9
+ def redis_daily_field_key(date_or_time)
10
+ date_key = date_or_time.strftime('%Y-%m')
11
+ [original_key, date_key].flatten.join(':')
12
+ end
13
+
14
+ def next_key(date, length)
15
+ date.next_month(length - 1)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/annual_counter'
4
+ class Redis
5
+ module Objects
6
+ module AnnualCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def annual_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::AnnualCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Objects
5
+ module Daily
6
+ module Counter
7
+ VERSION = '0.2.0'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/daily_counter'
4
+ class Redis
5
+ module Objects
6
+ module DailyCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def daily_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::DailyCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/hourly_counter'
4
+ class Redis
5
+ module Objects
6
+ module HourlyCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def hourly_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::HourlyCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/minutely_counter'
4
+ class Redis
5
+ module Objects
6
+ module MinutelyCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def minutely_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::MinutelyCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/daily_counter'
4
+ class Redis
5
+ module Objects
6
+ module MonthlyCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def monthly_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::MonthlyCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/weekly_counter'
4
+ class Redis
5
+ module Objects
6
+ module WeeklyCounters
7
+ class << self
8
+ def included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def weekly_counter(name, options = {}) # rubocop:disable Metrics/MethodLength
15
+ options[:start] ||= 0
16
+ options[:type] ||= (options[:start]).zero? ? :increment : :decrement
17
+ redis_objects[name.to_sym] = options.merge(type: :counter)
18
+
19
+ mod = Module.new do
20
+ define_method(name) do
21
+ Redis::WeeklyCounter.new(
22
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
23
+ )
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ else
35
+ include mod
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/base_counter_object"
4
+
5
+ class Redis
6
+ class WeeklyCounter < BaseCounterObject
7
+ private
8
+
9
+ def redis_daily_field_key(date_or_time)
10
+ date_key = date_or_time.strftime('%YW%W')
11
+ [original_key, date_key].flatten.join(':')
12
+ end
13
+
14
+ def next_key(date, length)
15
+ date + 7 * (length - 1)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis-objects'
4
+
5
+ class Redis
6
+ autoload :DailyCounter, 'redis/daily_counter'
7
+ autoload :WeeklyCounter, 'redis/weekly_counter'
8
+ autoload :MonthlyCounter, 'redis/monthly_counter'
9
+ autoload :AnnualCounter, 'redis/annual_counter'
10
+ autoload :HourlyCounter, 'redis/hourly_counter'
11
+ autoload :MinutelyCounter, 'redis/minutely_counter'
12
+
13
+ module Objects
14
+ autoload :DailyCounters, 'redis/objects/daily_counters'
15
+ autoload :WeeklyCounters, 'redis/objects/weekly_counters'
16
+ autoload :MonthlyCounters, 'redis/objects/monthly_counters'
17
+ autoload :AnnualCounters, 'redis/objects/annual_counters'
18
+ autoload :HourlyCounters, 'redis/objects/hourly_counters'
19
+ autoload :MinutelyCounters, 'redis/objects/minutely_counters'
20
+
21
+ class << self
22
+ alias original_included included
23
+
24
+ def included(klass)
25
+ original_included(klass)
26
+
27
+ # Pull in each object type
28
+ klass.send :include, Redis::Objects::DailyCounters
29
+ klass.send :include, Redis::Objects::WeeklyCounters
30
+ klass.send :include, Redis::Objects::MonthlyCounters
31
+ klass.send :include, Redis::Objects::AnnualCounters
32
+ klass.send :include, Redis::Objects::HourlyCounters
33
+ klass.send :include, Redis::Objects::MinutelyCounters
34
+ end
35
+ end
36
+ end
37
+ end