redis-objects-daily-counter 0.2.0

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