prop 1.2.0 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +47 -33
- data/lib/prop.rb +1 -1
- data/lib/prop/interval_strategy.rb +8 -7
- data/lib/prop/leaky_bucket_strategy.rb +21 -22
- data/lib/prop/limiter.rb +34 -29
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c346d4ae613e60059428af7761f47af474a02c63
|
4
|
+
data.tar.gz: 5eadca75e381659c036eaf348ff24181148d127b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce9e3393a3ff39720b1083a34d96b9c3c960f0ba5b47ab1f1cc7cb131132183880a6de7b0928a9736fb93c29ea1a81bd39abc79287783b5b85633bddf36edd48
|
7
|
+
data.tar.gz: 1ee56b82228280ef143b2cece612d2e310da75a99685259da374b05b6fe9a39810fb6fbecd006d260e9220c1b88e172b3793919fa1ca2ce4e1e4c26c889a3abc
|
data/README.md
CHANGED
@@ -1,30 +1,29 @@
|
|
1
1
|
|
2
2
|
# Prop [](https://travis-ci.org/zendesk/prop)
|
3
3
|
|
4
|
-
|
4
|
+
A gem to rate limit requests/actions of any kind.<br/>
|
5
|
+
Define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
|
5
6
|
|
6
7
|
Prop supports two limiting strategies:
|
7
8
|
|
8
|
-
* Basic strategy (default): Prop will use an interval to define a window of time using simple div arithmetic.
|
9
|
-
|
9
|
+
* Basic strategy (default): Prop will use an interval to define a window of time using simple div arithmetic.
|
10
|
+
This means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.
|
11
|
+
* Leaky bucket strategy: Prop also supports the [Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm,
|
12
|
+
which is similar to the basic strategy but also supports bursts up to a specified threshold.
|
10
13
|
|
11
|
-
To
|
14
|
+
To store values, prop needs a cache:
|
12
15
|
|
13
16
|
```ruby
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
Prop.write do |key, value|
|
19
|
-
Rails.cache.write(key, value)
|
20
|
-
end
|
17
|
+
# config/initializers/prop.rb
|
18
|
+
Prop.cache = Rails.cache # needs read/write/increment methods
|
21
19
|
```
|
22
20
|
|
23
|
-
|
21
|
+
Prop does not expire its used keys, so use memcached or similar, not redis.
|
24
22
|
|
25
23
|
## Setting a Callback
|
26
24
|
|
27
|
-
You can define an optional callback that is invoked when a rate limit is reached. In a Rails application you
|
25
|
+
You can define an optional callback that is invoked when a rate limit is reached. In a Rails application you
|
26
|
+
could use such a handler to add notification support:
|
28
27
|
|
29
28
|
```ruby
|
30
29
|
Prop.before_throttle do |handle, key, threshold, interval|
|
@@ -34,14 +33,12 @@ end
|
|
34
33
|
|
35
34
|
## Defining thresholds
|
36
35
|
|
37
|
-
|
36
|
+
Example: Limit on accepted emails per hour from a given user, by defining a threshold and interval (in seconds):
|
38
37
|
|
39
38
|
```ruby
|
40
39
|
Prop.configure(:mails_per_hour, threshold: 100, interval: 1.hour, description: "Mail rate limit exceeded")
|
41
40
|
```
|
42
41
|
|
43
|
-
The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with these values, by passing the handle to the respective methods in Prop:
|
44
|
-
|
45
42
|
```ruby
|
46
43
|
# Throws Prop::RateLimitExceededError if the threshold/interval has been reached
|
47
44
|
Prop.throttle!(:mails_per_hour)
|
@@ -52,18 +49,18 @@ Prop.throttle!(:expensive_request) { calculator.something_very_hard }
|
|
52
49
|
# Returns true if the threshold/interval has been reached
|
53
50
|
Prop.throttled?(:mails_per_hour)
|
54
51
|
|
55
|
-
# Sets the throttle
|
52
|
+
# Sets the throttle count to 0
|
56
53
|
Prop.reset(:mails_per_hour)
|
57
54
|
|
58
55
|
# Returns the value of this throttle, usually a count, but see below for more
|
59
56
|
Prop.count(:mails_per_hour)
|
60
57
|
```
|
61
58
|
|
62
|
-
Prop will raise a `
|
59
|
+
Prop will raise a `KeyError` if you attempt to operate on an undefined handle.
|
63
60
|
|
64
61
|
## Scoping a throttle
|
65
62
|
|
66
|
-
|
63
|
+
Example: scope the throttling to a specific sender rather than running a global "mails per hour" throttle:
|
67
64
|
|
68
65
|
```ruby
|
69
66
|
Prop.throttle!(:mails_per_hour, mail.from)
|
@@ -72,7 +69,7 @@ Prop.reset(:mails_per_hour, mail.from)
|
|
72
69
|
Prop.query(:mails_per_hour, mail.from)
|
73
70
|
```
|
74
71
|
|
75
|
-
The throttle scope can also be an array of values
|
72
|
+
The throttle scope can also be an array of values:
|
76
73
|
|
77
74
|
```ruby
|
78
75
|
Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
|
@@ -80,7 +77,10 @@ Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
|
|
80
77
|
|
81
78
|
## Error handling
|
82
79
|
|
83
|
-
If the
|
80
|
+
If the threshold for a given handle and key combination is exceeded, Prop throws a `Prop::RateLimited`.
|
81
|
+
This exception contains a "handle" reference and a "description" if specified during the configuration.
|
82
|
+
The handle allows you to rescue `Prop::RateLimited` and differentiate action depending on the handle.
|
83
|
+
For example, in Rails you can use this in e.g. `ApplicationController`:
|
84
84
|
|
85
85
|
```ruby
|
86
86
|
rescue_from Prop::RateLimited do |e|
|
@@ -94,15 +94,22 @@ end
|
|
94
94
|
|
95
95
|
### Using the Middleware
|
96
96
|
|
97
|
-
Prop ships with a built-in Rack middleware that you can use to do all the exception handling.
|
97
|
+
Prop ships with a built-in Rack middleware that you can use to do all the exception handling.
|
98
|
+
When a `Prop::RateLimited` error is caught, it will build an HTTP
|
99
|
+
[429 Too Many Requests](http://tools.ietf.org/html/draft-nottingham-http-new-status-02#section-4)
|
100
|
+
response and set the following headers:
|
98
101
|
|
99
102
|
Retry-After: 32
|
100
103
|
Content-Type: text/plain
|
101
104
|
Content-Length: 72
|
102
105
|
|
103
|
-
Where `Retry-After` is the number of seconds the client has to wait before retrying this end point.
|
106
|
+
Where `Retry-After` is the number of seconds the client has to wait before retrying this end point.
|
107
|
+
The body of this response is whatever description Prop has configured for the throttle that got violated,
|
108
|
+
or a default string if there's none configured.
|
104
109
|
|
105
|
-
If you wish to do manual error messaging in these cases, you can define an error handler in your Prop configuration.
|
110
|
+
If you wish to do manual error messaging in these cases, you can define an error handler in your Prop configuration.
|
111
|
+
Here's how the default error handler looks - you use anything that responds to `.call` and
|
112
|
+
takes the environment and a `RateLimited` instance as argument:
|
106
113
|
|
107
114
|
```ruby
|
108
115
|
error_handler = Proc.new do |env, error|
|
@@ -112,7 +119,7 @@ error_handler = Proc.new do |env, error|
|
|
112
119
|
[ 429, headers, [ body ]]
|
113
120
|
end
|
114
121
|
|
115
|
-
ActionController::Dispatcher.middleware.insert_before(ActionController::ParamsParser, :
|
122
|
+
ActionController::Dispatcher.middleware.insert_before(ActionController::ParamsParser, error_handler: error_handler)
|
116
123
|
```
|
117
124
|
|
118
125
|
An alternative to this, is to extend `Prop::Middleware` and override the `render_response(env, error)` method.
|
@@ -127,22 +134,23 @@ Prop.disabled do
|
|
127
134
|
end
|
128
135
|
```
|
129
136
|
|
130
|
-
##
|
137
|
+
## Overriding threshold
|
131
138
|
|
132
139
|
You can chose to override the threshold for a given key:
|
133
140
|
|
134
141
|
```ruby
|
135
|
-
Prop.throttle!(:mails_per_hour, mail.from, :
|
142
|
+
Prop.throttle!(:mails_per_hour, mail.from, threshold: current_account.mail_throttle_threshold)
|
136
143
|
```
|
137
144
|
|
138
|
-
When
|
145
|
+
When `throttle` is invoked without argument, the key is nil and as such a scope of its own, i.e. these are equivalent:
|
139
146
|
|
140
147
|
```ruby
|
141
148
|
Prop.throttle!(:mails_per_hour)
|
142
149
|
Prop.throttle!(:mails_per_hour, nil)
|
143
150
|
```
|
144
151
|
|
145
|
-
The default (and smallest possible) increment is 1, you can set that to any integer value using
|
152
|
+
The default (and smallest possible) increment is 1, you can set that to any integer value using
|
153
|
+
`:increment` which is handy for building time based throttles:
|
146
154
|
|
147
155
|
```ruby
|
148
156
|
Prop.configure(:execute_time, threshold: 10, interval: 1.minute)
|
@@ -173,18 +181,21 @@ rescue Prop::RateLimited => e
|
|
173
181
|
when :auth
|
174
182
|
raise AuthFailure
|
175
183
|
...
|
176
|
-
end
|
184
|
+
end
|
177
185
|
```
|
178
186
|
|
179
187
|
## Using Leaky Bucket Algorithm
|
180
188
|
|
181
|
-
You can add two additional configurations: `:strategy` and `:burst_rate` to use the
|
189
|
+
You can add two additional configurations: `:strategy` and `:burst_rate` to use the
|
190
|
+
[leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket).
|
191
|
+
Prop will handle the details after configured, and you don't have to specify `:strategy`
|
192
|
+
again when using `throttle`, `throttle!` or any other methods.
|
182
193
|
|
183
194
|
```ruby
|
184
195
|
Prop.configure(:api_request, strategy: :leaky_bucket, burst_rate: 20, threshold: 5, interval: 1.minute)
|
185
196
|
```
|
186
197
|
|
187
|
-
* `:threshold` value here would be the "leak rate" of leaky bucket algorithm.
|
198
|
+
* `:threshold` value here would be the "leak rate" of leaky bucket algorithm.
|
188
199
|
|
189
200
|
|
190
201
|
## License
|
@@ -196,4 +207,7 @@ You may obtain a copy of the License at
|
|
196
207
|
|
197
208
|
http://www.apache.org/licenses/LICENSE-2.0
|
198
209
|
|
199
|
-
Unless required by applicable law or agreed to in writing,
|
210
|
+
Unless required by applicable law or agreed to in writing,
|
211
|
+
software distributed under the License is distributed on an "AS IS" BASIS,
|
212
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
213
|
+
See the License for the specific language governing permissions and limitations under the License.
|
data/lib/prop.rb
CHANGED
@@ -7,7 +7,7 @@ module Prop
|
|
7
7
|
# Short hand for accessing Prop::Limiter methods
|
8
8
|
class << self
|
9
9
|
extend Forwardable
|
10
|
-
def_delegators :"Prop::Limiter", :read, :write, :configure, :configurations, :disabled, :before_throttle
|
10
|
+
def_delegators :"Prop::Limiter", :read, :write, :cache=, :configure, :configurations, :disabled, :before_throttle
|
11
11
|
def_delegators :"Prop::Limiter", :throttle, :throttle!, :throttled?, :count, :query, :reset
|
12
12
|
end
|
13
13
|
end
|
@@ -6,20 +6,21 @@ module Prop
|
|
6
6
|
class IntervalStrategy
|
7
7
|
class << self
|
8
8
|
def counter(cache_key, options)
|
9
|
-
Prop::Limiter.
|
9
|
+
Prop::Limiter.cache.read(cache_key).to_i
|
10
10
|
end
|
11
11
|
|
12
|
-
def increment(cache_key, options
|
12
|
+
def increment(cache_key, options)
|
13
13
|
increment = options.fetch(:increment, 1)
|
14
|
-
Prop::Limiter.
|
14
|
+
cache = Prop::Limiter.cache
|
15
|
+
cache.increment(cache_key, increment) || (cache.write(cache_key, increment, raw: true) && increment) # WARNING: potential race condition
|
15
16
|
end
|
16
17
|
|
17
18
|
def reset(cache_key)
|
18
|
-
Prop::Limiter.
|
19
|
+
Prop::Limiter.cache.write(cache_key, 0)
|
19
20
|
end
|
20
21
|
|
21
|
-
def
|
22
|
-
counter
|
22
|
+
def compare_threshold?(counter, operator, options)
|
23
|
+
counter.send operator, options.fetch(:threshold)
|
23
24
|
end
|
24
25
|
|
25
26
|
# Builds the expiring cache key
|
@@ -37,7 +38,7 @@ module Prop
|
|
37
38
|
def threshold_reached(options)
|
38
39
|
threshold = options.fetch(:threshold)
|
39
40
|
|
40
|
-
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s exceeded for key
|
41
|
+
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s exceeded for key #{options[:key].inspect}, hash #{options[:cache_key]}"
|
41
42
|
end
|
42
43
|
|
43
44
|
def validate_options!(options)
|
@@ -6,34 +6,31 @@ require 'prop/interval_strategy'
|
|
6
6
|
module Prop
|
7
7
|
class LeakyBucketStrategy
|
8
8
|
class << self
|
9
|
-
def
|
10
|
-
bucket = Prop::Limiter.
|
9
|
+
def counter(cache_key, options)
|
10
|
+
bucket = Prop::Limiter.cache.read(cache_key) || default_bucket
|
11
11
|
now = Time.now.to_i
|
12
|
-
leak_amount = (now - bucket
|
12
|
+
leak_amount = (now - bucket.fetch(:last_updated)) / options.fetch(:interval) * options.fetch(:threshold)
|
13
13
|
|
14
|
-
bucket[:bucket] = [bucket
|
14
|
+
bucket[:bucket] = [bucket.fetch(:bucket) - leak_amount, 0].max
|
15
15
|
bucket[:last_updated] = now
|
16
|
-
|
17
|
-
Prop::Limiter.writer.call(cache_key, bucket)
|
18
16
|
bucket
|
19
17
|
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
Prop::Limiter.writer.call(cache_key, bucket)
|
19
|
+
# WARNING: race condition
|
20
|
+
# this increment is not atomic, so it might miss counts when used frequently
|
21
|
+
def increment(cache_key, options)
|
22
|
+
counter = counter(cache_key, options)
|
23
|
+
counter[:bucket] += options.fetch(:increment, 1)
|
24
|
+
Prop::Limiter.cache.write(cache_key, counter)
|
25
|
+
counter
|
29
26
|
end
|
30
27
|
|
31
28
|
def reset(cache_key)
|
32
|
-
Prop::Limiter.
|
29
|
+
Prop::Limiter.cache.write(cache_key, default_bucket)
|
33
30
|
end
|
34
31
|
|
35
|
-
def
|
36
|
-
counter
|
32
|
+
def compare_threshold?(counter, operator, options)
|
33
|
+
counter.fetch(:bucket).to_i.send operator, options.fetch(:burst_rate)
|
37
34
|
end
|
38
35
|
|
39
36
|
def build(options)
|
@@ -45,15 +42,11 @@ module Prop
|
|
45
42
|
"prop/leaky_bucket/#{Digest::MD5.hexdigest(cache_key)}"
|
46
43
|
end
|
47
44
|
|
48
|
-
def default_bucket
|
49
|
-
{ :bucket => 0, :last_updated => 0 }
|
50
|
-
end
|
51
|
-
|
52
45
|
def threshold_reached(options)
|
53
46
|
burst_rate = options.fetch(:burst_rate)
|
54
47
|
threshold = options.fetch(:threshold)
|
55
48
|
|
56
|
-
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s and burst rate #{burst_rate} tries exceeded for key
|
49
|
+
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s and burst rate #{burst_rate} tries exceeded for key #{options[:key].inspect}, hash #{options[:cache_key]}"
|
57
50
|
end
|
58
51
|
|
59
52
|
def validate_options!(options)
|
@@ -63,6 +56,12 @@ module Prop
|
|
63
56
|
raise ArgumentError.new(":burst_rate must be an Integer and larger than :threshold")
|
64
57
|
end
|
65
58
|
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def default_bucket
|
63
|
+
{ bucket: 0, last_updated: 0 }
|
64
|
+
end
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
data/lib/prop/limiter.rb
CHANGED
@@ -8,14 +8,22 @@ module Prop
|
|
8
8
|
class Limiter
|
9
9
|
|
10
10
|
class << self
|
11
|
-
attr_accessor :handles, :
|
11
|
+
attr_accessor :handles, :before_throttle_callback, :cache
|
12
12
|
|
13
13
|
def read(&blk)
|
14
|
-
|
14
|
+
raise "Use .cache = "
|
15
15
|
end
|
16
16
|
|
17
17
|
def write(&blk)
|
18
|
-
|
18
|
+
raise "Use .cache = "
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache=(cache)
|
22
|
+
[:read, :write, :increment].each do |method|
|
23
|
+
next if cache.respond_to?(method)
|
24
|
+
raise ArgumentError, "Cache needs to respond to #{method}"
|
25
|
+
end
|
26
|
+
@cache = cache
|
19
27
|
end
|
20
28
|
|
21
29
|
def before_throttle(&blk)
|
@@ -25,12 +33,12 @@ module Prop
|
|
25
33
|
# Public: Registers a handle for rate limiting
|
26
34
|
#
|
27
35
|
# handle - the name of the handle you wish to use in your code, e.g. :login_attempt
|
28
|
-
# defaults - the settings for this handle, e.g. { :
|
36
|
+
# defaults - the settings for this handle, e.g. { threshold: 5, interval: 5.minutes }
|
29
37
|
#
|
30
38
|
# Raises Prop::RateLimited if the number if the threshold for this handle has been reached
|
31
39
|
def configure(handle, defaults)
|
32
|
-
raise
|
33
|
-
raise
|
40
|
+
raise ArgumentError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
|
41
|
+
raise ArgumentError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
|
34
42
|
|
35
43
|
self.handles ||= {}
|
36
44
|
self.handles[handle] = defaults
|
@@ -55,23 +63,19 @@ module Prop
|
|
55
63
|
#
|
56
64
|
# Returns true if the threshold for this handle has been reached, else returns false
|
57
65
|
def throttle(handle, key = nil, options = {})
|
58
|
-
|
59
|
-
counter = @strategy.counter(cache_key, options)
|
66
|
+
return false if disabled?
|
60
67
|
|
61
|
-
|
62
|
-
|
63
|
-
unless before_throttle_callback.nil?
|
64
|
-
before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
|
65
|
-
end
|
66
|
-
|
67
|
-
true
|
68
|
-
else
|
69
|
-
@strategy.increment(cache_key, options, counter)
|
68
|
+
options, cache_key = prepare(handle, key, options)
|
69
|
+
counter = @strategy.increment(cache_key, options)
|
70
70
|
|
71
|
-
|
71
|
+
if @strategy.compare_threshold?(counter, :>, options)
|
72
|
+
before_throttle_callback &&
|
73
|
+
before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
+
true
|
76
|
+
else
|
77
|
+
yield if block_given?
|
78
|
+
false
|
75
79
|
end
|
76
80
|
end
|
77
81
|
|
@@ -82,19 +86,19 @@ module Prop
|
|
82
86
|
# options - request specific overrides to the defaults configured for this handle
|
83
87
|
# (optional) a block of code that this throttle is guarding
|
84
88
|
#
|
85
|
-
# Raises Prop::RateLimited if the
|
89
|
+
# Raises Prop::RateLimited if the threshold for this handle has been reached
|
86
90
|
# Returns the value of the block if given a such, otherwise the current count of the throttle
|
87
91
|
def throttle!(handle, key = nil, options = {})
|
88
92
|
options, cache_key = prepare(handle, key, options)
|
89
93
|
|
90
94
|
if throttle(handle, key, options)
|
91
|
-
raise Prop::RateLimited.new(options.merge(:
|
95
|
+
raise Prop::RateLimited.new(options.merge(cache_key: cache_key, handle: handle))
|
92
96
|
end
|
93
97
|
|
94
98
|
block_given? ? yield : @strategy.counter(cache_key, options)
|
95
99
|
end
|
96
100
|
|
97
|
-
# Public:
|
101
|
+
# Public: Is the given handle/key combination currently throttled ?
|
98
102
|
#
|
99
103
|
# handle - the throttle identifier
|
100
104
|
# key - the associated key
|
@@ -103,7 +107,7 @@ module Prop
|
|
103
107
|
def throttled?(handle, key = nil, options = {})
|
104
108
|
options, cache_key = prepare(handle, key, options)
|
105
109
|
counter = @strategy.counter(cache_key, options)
|
106
|
-
@strategy.
|
110
|
+
@strategy.compare_threshold?(counter, :>=, options)
|
107
111
|
end
|
108
112
|
|
109
113
|
# Public: Resets a specific throttle
|
@@ -113,7 +117,7 @@ module Prop
|
|
113
117
|
#
|
114
118
|
# Returns nothing
|
115
119
|
def reset(handle, key = nil, options = {})
|
116
|
-
|
120
|
+
_options, cache_key = prepare(handle, key, options)
|
117
121
|
@strategy.reset(cache_key)
|
118
122
|
end
|
119
123
|
|
@@ -141,14 +145,15 @@ module Prop
|
|
141
145
|
end
|
142
146
|
|
143
147
|
def prepare(handle, key, params)
|
144
|
-
|
148
|
+
unless defaults = handles[handle]
|
149
|
+
raise KeyError.new("No such handle configured: #{handle.inspect}")
|
150
|
+
end
|
145
151
|
|
146
|
-
|
147
|
-
options = Prop::Options.build(:key => key, :params => params, :defaults => defaults)
|
152
|
+
options = Prop::Options.build(key: key, params: params, defaults: defaults)
|
148
153
|
|
149
154
|
@strategy = options.fetch(:strategy)
|
150
155
|
|
151
|
-
cache_key = @strategy.build(:
|
156
|
+
cache_key = @strategy.build(key: key, handle: handle, interval: options[:interval])
|
152
157
|
|
153
158
|
[ options, cache_key ]
|
154
159
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Morten Primdahl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: maxitest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: mocha
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,7 +53,21 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bump
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - ">="
|
@@ -102,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
116
|
version: '0'
|
103
117
|
requirements: []
|
104
118
|
rubyforge_project:
|
105
|
-
rubygems_version: 2.4.
|
119
|
+
rubygems_version: 2.4.5.1
|
106
120
|
signing_key:
|
107
121
|
specification_version: 4
|
108
122
|
summary: Gem for implementing rate limits.
|