rate-limit 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +25 -66
- data/lib/rate-limit.rb +3 -0
- data/lib/rate_limit/base.rb +6 -14
- data/lib/rate_limit/config.rb +10 -0
- data/lib/rate_limit/errors/limit_exceeded_error.rb +5 -5
- data/lib/rate_limit/result.rb +28 -0
- data/lib/rate_limit/throttler.rb +7 -48
- data/lib/rate_limit/version.rb +1 -1
- data/lib/rate_limit/window.rb +10 -6
- data/lib/rate_limit/worker.rb +49 -0
- data/lib/rate_limit.rb +2 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0f1786c2b5fde684e916bcc8733929b7d0d35cdc2807d8f511190f179019611
|
4
|
+
data.tar.gz: c216ce4a4f8e2e479854b61b895c87d226cceba9f8a3d6fb476852709a308ed2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25bbef68dd61abd1cb2353367af910ee5d61d2ce9f1408c66a36a2741abab62bc9b3c212552e01187c5a95121c611eeb9b16232b1b1bdcc4e486fcdc780b85e0
|
7
|
+
data.tar.gz: 5232a6c6f559c5161576b1f54d27a17b590c3b2d25376f8b6f0011d725dbf029bbc7b75e78d4afc6babeaea3a0acac4b9c3b643738f4e9730fbc5105241f208c
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog],
|
6
|
+
and this project adheres to [Semantic Versioning].
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [v0.2.0] - 2022-10-05
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- added `RateLimit::Errors::LimitExceededError#result`
|
15
|
+
- added `Worker#raise_errors`
|
16
|
+
- added `Worker#only_failures`
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
|
20
|
+
- changed `RateLimit::Result.initialize`
|
21
|
+
|
22
|
+
### Removed
|
23
|
+
|
24
|
+
- removed `RateLimit::Result#namespace`
|
25
|
+
- removed `RateLimit::Worker#namespace`
|
26
|
+
- removed `RateLimit::Errors::LimitExceededError#namespace`
|
27
|
+
- removed `RateLimit::Errors::LimitExceededError#worker`
|
28
|
+
- removed `RateLimit.throttle_with_block!` and `Worker#throttle_with_block!` in favour of `.throttle` with `{ raise_errors: true }` option
|
29
|
+
- removed `RateLimit.throttle_only_failures_with_block!` and `Worker#throttle_only_failures_with_block!` in favour of `.throttle` with `{ only_failures: true }` option
|
30
|
+
|
31
|
+
|
32
|
+
### Fixed
|
33
|
+
|
34
|
+
- https://github.com/catawiki/rate-limit/issues/16 String Values are not respected
|
35
|
+
|
36
|
+
## [v0.1.0] - 2022-09-19
|
37
|
+
|
38
|
+
|
39
|
+
### Added
|
40
|
+
|
41
|
+
- https://github.com/catawiki/rate-limit/pull/11 `RateLimit::Result` class
|
42
|
+
- https://github.com/catawiki/rate-limit/pull/12 `RateLimit::Worker` class
|
43
|
+
- https://github.com/catawiki/rate-limit/pull/13 `RateLimit::Config#on_success` and `RateLimit::Config#on_failure`
|
44
|
+
|
45
|
+
### Changed
|
46
|
+
|
47
|
+
- `RateLimit.throttle` to not accept block
|
48
|
+
- `RateLimit.throttle` to return `RateLimit::Result` object
|
49
|
+
- `RateLimit::Throttler` from class to module while moving responsibilities to `RateLimit::Worker` class
|
50
|
+
- renamed `RateLimit.throttle!` to `RateLimit.throttle_with_block!`
|
51
|
+
- renamed `RateLimit.throttle_only_failures` to `RateLimit.throttle_only_failures_with_block!`
|
52
|
+
|
53
|
+
### Fixed
|
54
|
+
|
55
|
+
- https://github.com/catawiki/rate-limit/issues/7 Symbol topic names does not load the correct limits
|
56
|
+
- https://github.com/catawiki/rate-limit/issues/6 Main Module (RateLimit) fails to autoload
|
57
|
+
|
58
|
+
|
59
|
+
## v0.0.1 - 2022-09-09
|
60
|
+
|
61
|
+
- Initial gem release
|
62
|
+
|
63
|
+
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
64
|
+
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
65
|
+
|
66
|
+
<!-- versions -->
|
67
|
+
|
68
|
+
[Unreleased]: https://github.com/catawiki/rate-limit/compare/v0.1.0...HEAD
|
69
|
+
[v0.2.0]: https://github.com/catawiki/rate-limit/compare/v0.1.0...v0.2.0
|
70
|
+
[v0.1.0]: https://github.com/catawiki/rate-limit/compare/v0.0.1...v0.1.0
|
data/README.md
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
|
4
4
|
Protect your Ruby apps from bad actors. RateLimit allows you to set permissions as to whether certain number of feature calls are valid or not for a specific entity (user, phone number, email address, etc...).
|
5
5
|
|
6
|
-
This gem mainly provides brute-force protection by throttling
|
6
|
+
This gem mainly provides brute-force protection by throttling attempts for a specific entity id (i.e user_id). However it could also be used to throttle based on ip address (we recommend that you consider using [Rack::Attack](https://github.com/rack/rack-attack) for more optimized ip throttling)
|
7
7
|
|
8
8
|
#### Common Use Cases
|
9
9
|
* [Login] Brute-force attempts for a spefic account
|
10
|
-
* [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a
|
11
|
-
* [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a
|
10
|
+
* [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a specific user_id
|
11
|
+
* [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a specific phone_number
|
12
12
|
* [Verifications] Brute-force attempts for entering verification codes
|
13
13
|
* [Redeem] Brute-force attempts to redeem voucher codes from a specific account
|
14
14
|
|
@@ -28,81 +28,32 @@ Or install it yourself as:
|
|
28
28
|
|
29
29
|
$ gem install rate-limit
|
30
30
|
|
31
|
-
## Usage
|
31
|
+
## Basic Usage
|
32
32
|
|
33
|
-
|
33
|
+
### [`RateLimit.throttle`](https://github.com/catawiki/rate-limit/wiki/Throttling)
|
34
|
+
The throttle method expects the following options
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
```
|
40
|
-
or
|
36
|
+
| Option | Description | Examples |
|
37
|
+
| ---------------- | ------------------------------------------------------------------------------- | ------------------------------------- |
|
38
|
+
| topic | The topic you would like to throttle | "login", "send_sms", "redeem_voucher" |
|
39
|
+
| value | The identifier of the unique entity that is throttled based on the topic limits | user_id, phone_number, voucher_code |
|
41
40
|
|
42
|
-
```ruby
|
43
|
-
if RateLimit.throttle(topic: :login, value: id)
|
44
|
-
# Do something
|
45
|
-
end
|
46
|
-
```
|
47
41
|
|
48
|
-
|
42
|
+
The `throttle` method checks if the given value did exceed the defined limits for the given topic. If the limit is exceeded then it returns [RateLimit::Result](https://github.com/catawiki/rate-limit/wiki/RateLimit::Result) Object, where `result.success?` will be `false`. Otherwise, it increments the attempts counter in the cache and sets `result.success?` to `true`.
|
49
43
|
|
44
|
+
#### Example
|
50
45
|
```ruby
|
51
|
-
|
52
|
-
RateLimit.throttle!(topic: :send_sms, namespace: :user_id, value: id) do
|
53
|
-
# Logic goes Here
|
54
|
-
end
|
55
|
-
rescue RateLimit::Errors::LimitExceededError => e
|
56
|
-
# Error Handling Logic goes here
|
57
|
-
e.topic # :login
|
58
|
-
e.namespace # :user_id
|
59
|
-
e.value # id
|
60
|
-
e.threshold # 2
|
61
|
-
e.interval # 60
|
62
|
-
end
|
63
|
-
```
|
64
|
-
|
65
|
-
#### Advanced
|
46
|
+
result = RateLimit.throttle(topic: :login, value: 123)
|
66
47
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
begin
|
71
|
-
throttler.perform! do
|
72
|
-
# Logic goes Here
|
73
|
-
end
|
74
|
-
rescue RateLimit::Errors::LimitExceededError => e
|
75
|
-
# Error Handling Logic goes here
|
76
|
-
end
|
77
|
-
```
|
78
|
-
|
79
|
-
#### Manual
|
80
|
-
|
81
|
-
```ruby
|
82
|
-
throttler = RateLimit::Throttler.new(topic: :login, namespace: :user_id, value: id)
|
83
|
-
|
84
|
-
unless throttler.limit_exceeded?
|
85
|
-
# Logic goes Here
|
86
|
-
|
87
|
-
throttler.increment_counters
|
48
|
+
if result.success?
|
49
|
+
# Do something
|
88
50
|
end
|
89
51
|
```
|
90
52
|
|
91
|
-
#### Nested throttles
|
92
53
|
|
93
|
-
|
94
|
-
begin
|
95
|
-
RateLimit.throttle!(topic: :send_sms, namespace: :user_id, value: id) do
|
96
|
-
RateLimit.throttle!(topic: :send_sms, namespace: :phone_number, value: number) do
|
97
|
-
# Logic goes Here
|
98
|
-
end
|
99
|
-
end
|
100
|
-
rescue RateLimit::Errors::LimitExceededError => e
|
101
|
-
# Error Handling Logic goes here
|
102
|
-
end
|
103
|
-
```
|
54
|
+
Please check the [Wiki](https://github.com/catawiki/rate-limit/wiki) for advanced throttling and options.
|
104
55
|
|
105
|
-
|
56
|
+
## [Configuration](https://github.com/catawiki/rate-limit/wiki/Configuration)
|
106
57
|
|
107
58
|
Customize the configuration by adding the following block to `config/initializers/rate_limit.rb`
|
108
59
|
|
@@ -113,6 +64,14 @@ RateLimit.configure do |config|
|
|
113
64
|
config.default_interval = 60
|
114
65
|
config.default_threshold = 2
|
115
66
|
config.limits_file_path = 'config/rate-limit.yml'
|
67
|
+
config.on_success = proc { |result|
|
68
|
+
# Success Logic Goes HERE
|
69
|
+
# result.topic, result.value
|
70
|
+
}
|
71
|
+
config.on_failure = proc { |result|
|
72
|
+
# Failure Logic Goes HERE
|
73
|
+
# result.topic, result.value, result.threshold, result.interval
|
74
|
+
}
|
116
75
|
end
|
117
76
|
```
|
118
77
|
|
data/lib/rate-limit.rb
ADDED
data/lib/rate_limit/base.rb
CHANGED
@@ -3,29 +3,21 @@
|
|
3
3
|
module RateLimit
|
4
4
|
module Base
|
5
5
|
def throttle(**args)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
def throttle!(**args)
|
12
|
-
Throttler.new(**args).perform! { yield if block_given? }
|
13
|
-
end
|
14
|
-
|
15
|
-
def throttle_only_failures!(**args)
|
16
|
-
Throttler.new(**args).perform_only_failures! { yield if block_given? }
|
6
|
+
worker = Worker.new(**args)
|
7
|
+
worker.throttle { yield if block_given? }
|
8
|
+
worker.result
|
17
9
|
end
|
18
10
|
|
19
11
|
def limit_exceeded?(**args)
|
20
|
-
|
12
|
+
Worker.new(**args).reloaded_limit_exceeded?
|
21
13
|
end
|
22
14
|
|
23
15
|
def reset_counters(**args)
|
24
|
-
|
16
|
+
Worker.new(**args).clear_cache_counter
|
25
17
|
end
|
26
18
|
|
27
19
|
def increment_counters(**args)
|
28
|
-
|
20
|
+
Worker.new(**args).increment_cache_counter
|
29
21
|
end
|
30
22
|
end
|
31
23
|
end
|
data/lib/rate_limit/config.rb
CHANGED
@@ -9,6 +9,8 @@ module RateLimit
|
|
9
9
|
attr_accessor :default_interval,
|
10
10
|
:default_threshold,
|
11
11
|
:limits_file_path,
|
12
|
+
:on_success,
|
13
|
+
:on_failure,
|
12
14
|
:fail_safe,
|
13
15
|
:redis
|
14
16
|
|
@@ -24,6 +26,14 @@ module RateLimit
|
|
24
26
|
raw_limits[topic] || Defaults.raw_limits
|
25
27
|
end
|
26
28
|
|
29
|
+
def success_callback(*args)
|
30
|
+
on_success&.call(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure_callback(*args)
|
34
|
+
on_failure&.call(*args)
|
35
|
+
end
|
36
|
+
|
27
37
|
private
|
28
38
|
|
29
39
|
def raw_limits
|
@@ -3,18 +3,18 @@
|
|
3
3
|
module RateLimit
|
4
4
|
module Errors
|
5
5
|
class LimitExceededError < StandardError
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :result
|
7
7
|
|
8
|
-
delegate :topic, :
|
8
|
+
delegate :topic, :value, :threshold, :interval, to: :result
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
10
|
+
def initialize(result)
|
11
|
+
@result = result
|
12
12
|
|
13
13
|
super(custom_message)
|
14
14
|
end
|
15
15
|
|
16
16
|
def custom_message
|
17
|
-
"#{topic}:
|
17
|
+
"#{result.topic}: has exceeded #{result.threshold} in #{result.interval} seconds"
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RateLimit
|
4
|
+
class Result
|
5
|
+
# Attributes
|
6
|
+
attr_accessor :topic, :value, :threshold, :interval
|
7
|
+
|
8
|
+
# Methods
|
9
|
+
def initialize(topic:, value:)
|
10
|
+
@topic = topic
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
@success
|
16
|
+
end
|
17
|
+
|
18
|
+
def success!
|
19
|
+
@success = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def failure!(worker)
|
23
|
+
@success = false
|
24
|
+
@threshold = worker.exceeded_window&.threshold
|
25
|
+
@interval = worker.exceeded_window&.interval
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/rate_limit/throttler.rb
CHANGED
@@ -1,57 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RateLimit
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(topic:, value:, namespace: nil)
|
8
|
-
@topic = topic.to_s
|
9
|
-
@value = value.to_i
|
10
|
-
@namespace = namespace&.to_s
|
11
|
-
@windows = Limit.fetch(topic).map { |limit| Window.new(self, limit) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def perform!
|
15
|
-
validate_limit!
|
4
|
+
module Throttler
|
5
|
+
def throttle
|
6
|
+
return failure! if reloaded_limit_exceeded?
|
16
7
|
|
17
8
|
yield if block_given?
|
18
9
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def perform_only_failures!
|
25
|
-
validate_limit!
|
26
|
-
|
27
|
-
begin
|
28
|
-
yield if block_given?
|
29
|
-
rescue StandardError => e
|
30
|
-
increment_cache_counter
|
31
|
-
raise e
|
32
|
-
end
|
33
|
-
|
34
|
-
true
|
35
|
-
end
|
36
|
-
|
37
|
-
def limit_exceeded?
|
38
|
-
Window.find_exceeded(windows).present?
|
39
|
-
end
|
40
|
-
|
41
|
-
def increment_cache_counter
|
42
|
-
Window.increment_cache_counter(windows)
|
43
|
-
end
|
44
|
-
|
45
|
-
def clear_cache_counter
|
46
|
-
Window.clear_cache_counter(windows)
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def validate_limit!
|
52
|
-
exceeded_window = Window.find_exceeded(windows)
|
53
|
-
|
54
|
-
raise Errors::LimitExceededError, exceeded_window if exceeded_window
|
10
|
+
return success! unless only_failures
|
11
|
+
rescue StandardError => e
|
12
|
+
success! unless e.is_a?(Errors::LimitExceededError)
|
13
|
+
raise e
|
55
14
|
end
|
56
15
|
end
|
57
16
|
end
|
data/lib/rate_limit/version.rb
CHANGED
data/lib/rate_limit/window.rb
CHANGED
@@ -2,18 +2,18 @@
|
|
2
2
|
|
3
3
|
module RateLimit
|
4
4
|
class Window
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :worker, :limit
|
6
6
|
|
7
|
-
delegate :topic, :
|
7
|
+
delegate :topic, :value, to: :worker
|
8
8
|
delegate :threshold, :interval, to: :limit
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
12
|
-
@limit
|
10
|
+
def initialize(worker, limit)
|
11
|
+
@worker = worker
|
12
|
+
@limit = limit
|
13
13
|
end
|
14
14
|
|
15
15
|
def key
|
16
|
-
@key ||= [topic,
|
16
|
+
@key ||= [topic, value, interval].join(':')
|
17
17
|
end
|
18
18
|
|
19
19
|
def cached_counter
|
@@ -21,6 +21,10 @@ module RateLimit
|
|
21
21
|
end
|
22
22
|
|
23
23
|
class << self
|
24
|
+
def find_all(topic:, worker:)
|
25
|
+
Limit.fetch(topic).map { |limit| Window.new(worker, limit) }
|
26
|
+
end
|
27
|
+
|
24
28
|
def find_exceeded(windows)
|
25
29
|
windows.find { |w| w.cached_counter >= w.threshold }
|
26
30
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RateLimit
|
4
|
+
class Worker
|
5
|
+
include Throttler
|
6
|
+
|
7
|
+
attr_accessor :topic, :value, :limits, :windows, :exceeded_window, :result, :raise_errors, :only_failures
|
8
|
+
|
9
|
+
def initialize(topic:, value:, raise_errors: false, only_failures: false)
|
10
|
+
@topic = topic.to_s
|
11
|
+
@value = value.to_s
|
12
|
+
@windows = Window.find_all(worker: self, topic: @topic)
|
13
|
+
@result = Result.new(topic: @topic, value: @value)
|
14
|
+
@raise_errors = raise_errors
|
15
|
+
@only_failures = only_failures
|
16
|
+
end
|
17
|
+
|
18
|
+
def increment_cache_counter
|
19
|
+
Window.increment_cache_counter(windows)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear_cache_counter
|
23
|
+
Window.clear_cache_counter(windows)
|
24
|
+
end
|
25
|
+
|
26
|
+
def reloaded_limit_exceeded?
|
27
|
+
@exceeded_window = Window.find_exceeded(windows)
|
28
|
+
|
29
|
+
limit_exceeded?
|
30
|
+
end
|
31
|
+
|
32
|
+
def limit_exceeded?
|
33
|
+
exceeded_window.present?
|
34
|
+
end
|
35
|
+
|
36
|
+
def success!
|
37
|
+
increment_cache_counter
|
38
|
+
result.success!
|
39
|
+
RateLimit.config.success_callback(result)
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure!
|
43
|
+
result.failure!(self)
|
44
|
+
RateLimit.config.failure_callback(result)
|
45
|
+
|
46
|
+
raise Errors::LimitExceededError, result if raise_errors
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/rate_limit.rb
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'active_support/core_ext/module'
|
4
4
|
require_relative 'rate_limit/configurable'
|
5
|
+
require_relative 'rate_limit/result'
|
5
6
|
require_relative 'rate_limit/cache'
|
6
7
|
require_relative 'rate_limit/window'
|
7
8
|
require_relative 'rate_limit/throttler'
|
9
|
+
require_relative 'rate_limit/worker'
|
8
10
|
require_relative 'rate_limit/limit'
|
9
11
|
require_relative 'rate_limit/errors/limit_exceeded_error'
|
10
12
|
require_relative 'rate_limit/base'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rate-limit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mohamed Motaweh
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- CHANGELOG.md
|
61
61
|
- LICENSE
|
62
62
|
- README.md
|
63
|
+
- lib/rate-limit.rb
|
63
64
|
- lib/rate_limit.rb
|
64
65
|
- lib/rate_limit/base.rb
|
65
66
|
- lib/rate_limit/cache.rb
|
@@ -69,9 +70,11 @@ files:
|
|
69
70
|
- lib/rate_limit/configurable.rb
|
70
71
|
- lib/rate_limit/errors/limit_exceeded_error.rb
|
71
72
|
- lib/rate_limit/limit.rb
|
73
|
+
- lib/rate_limit/result.rb
|
72
74
|
- lib/rate_limit/throttler.rb
|
73
75
|
- lib/rate_limit/version.rb
|
74
76
|
- lib/rate_limit/window.rb
|
77
|
+
- lib/rate_limit/worker.rb
|
75
78
|
homepage: https://github.com/catawiki/rate-limit
|
76
79
|
licenses:
|
77
80
|
- MIT
|
@@ -95,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
98
|
- !ruby/object:Gem::Version
|
96
99
|
version: '0'
|
97
100
|
requirements: []
|
98
|
-
rubygems_version: 3.
|
101
|
+
rubygems_version: 3.2.3
|
99
102
|
signing_key:
|
100
103
|
specification_version: 4
|
101
104
|
summary: A Rate Limiting Gem
|