prop 0.6.6 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  Prop is a simple gem for rate limiting requests of any kind. It allows you to configure hooks for registering certain actions, such that you can define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
5
5
 
6
+ Prop uses and interval to define a window of time using simple div arithmetic. This means that it's a worst case throttle that will allow up to 2 times the specified requests within the specified interval.
7
+
6
8
  To get going with Prop you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example do something like the below in a Rails initializer:
7
9
 
8
10
  Prop.read do |key|
@@ -13,7 +15,7 @@ To get going with Prop you first define the read and write operations. These def
13
15
  Rails.cache.write(key, value)
14
16
  end
15
17
 
16
- You can choose to rely on a database or Moneta or Redis or whatever you'd like to use for transient storage. Prop does not do any sort of clean up of its key space, so you would have to implement that manually should you be using anything but an LRU cache.
18
+ You can choose to rely on whatever you'd like to use for transient storage. Prop does not do any sort of clean up of its key space, so you would have to implement that manually should you be using anything but an LRU cache like memcached.
17
19
 
18
20
  ## Defining thresholds
19
21
 
@@ -21,7 +23,7 @@ Once the read and write operations are defined, you can optionally define thresh
21
23
 
22
24
  Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
23
25
 
24
- You can now put the throttle to work with this values, by passing the "handle" to the respective methods in Prop:
26
+ The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with this values, by passing the handle to the respective methods in Prop:
25
27
 
26
28
  # Throws Prop::RateLimitExceededError if the threshold/interval has been reached
27
29
  Prop.throttle!(:mails_per_hour)
@@ -33,7 +35,7 @@ You can now put the throttle to work with this values, by passing the "handle" t
33
35
  Prop.reset(:mails_per_hour)
34
36
 
35
37
  # Returns the value of this throttle, usually a count, but see below for more
36
- Prop.query(:mails_per_hour)
38
+ Prop.count(:mails_per_hour)
37
39
 
38
40
  Prop will raise a RuntimeError if you attempt to operate on an undefined handle.
39
41
 
@@ -52,7 +54,7 @@ The throttle scope can also be an array of values, e.g.:
52
54
 
53
55
  ## Error handling
54
56
 
55
- If the throttle! method gets called more than "threshold" times within "interval in seconds" for a given handle and key combination, Prop throws a Prop::RateLimitExceededError. This exception contains a "handle" reference and a "description" if specified during the configuration. The handle allows you to rescue Prop::RateLimitExceededError and differentiate action depending on the handle. For example, in Rails you can use this in e.g. ApplicationController:
57
+ If the throttle! method gets called more than "threshold" times within "interval in seconds" for a given handle and key combination, Prop throws a Prop::RateLimited error which is a subclass of StandardError. This exception contains a "handle" reference and a "description" if specified during the configuration. The handle allows you to rescue Prop::RateLimited and differentiate action depending on the handle. For example, in Rails you can use this in e.g. ApplicationController:
56
58
 
57
59
  rescue_from Prop::RateLimitExceededError do |e|
58
60
  if e.handle == :authorization_attempt
@@ -86,17 +88,3 @@ The default (and smallest possible) increment is 1, you can set that to any inte
86
88
  Prop.setup(:execute_time, :threshold => 10, :interval => 1.minute)
87
89
  Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
88
90
 
89
- ## How it works
90
-
91
- Prop uses the interval to define a window of time using simple div arithmetic. This means that it's a worst case throttle that will allow up to 2 times the specified requests within the specified interval.
92
-
93
- ## Note on Patches/Pull Requests
94
-
95
- * Fork the project.
96
- * Make your feature addition or bug fix.
97
- * Add tests for it. This is important so I don't break it in a
98
- future version unintentionally.
99
- * Commit, do not mess with rakefile, version, or history.
100
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
101
- * Send me a pull request. Bonus points for topic branches.
102
-
data/lib/prop.rb CHANGED
@@ -1,124 +1,13 @@
1
- require 'digest/md5'
1
+ require "prop/limiter"
2
+ require "forwardable"
2
3
 
3
- class Object
4
- def define_prop_class_method(name, &blk)
5
- (class << self; self; end).instance_eval { define_method(name, &blk) }
6
- end
7
- end
8
-
9
- class Prop
10
- VERSION = "0.6.6"
11
-
12
- class RateLimitExceededError < RuntimeError
13
- attr_accessor :handle, :retry_after, :description
14
-
15
- def self.create(handle, key, threshold, description = nil)
16
- error = new("#{handle} threshold of #{threshold} exceeded for key '#{key}'")
17
- error.description = description
18
- error.handle = handle
19
- error.retry_after = threshold - Time.now.to_i % threshold if threshold > 0
20
-
21
- raise error
22
- end
23
- end
4
+ module Prop
5
+ VERSION = "0.7.0"
24
6
 
7
+ # Short hand for accessing Prop::Limiter methods
25
8
  class << self
26
- attr_accessor :handles, :reader, :writer
27
-
28
- def read(&blk)
29
- self.reader = blk
30
- end
31
-
32
- def write(&blk)
33
- self.writer = blk
34
- end
35
-
36
- def configure(handle, defaults)
37
- raise RuntimeError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
38
- raise RuntimeError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
39
-
40
- self.handles ||= {}
41
- self.handles[handle] = defaults
42
- end
43
-
44
- def disabled(&block)
45
- @disabled = true
46
- yield
47
- ensure
48
- @disabled = false
49
- end
50
-
51
- def disabled?
52
- !!@disabled
53
- end
54
-
55
- def throttle!(handle, key = nil, options = {})
56
- options = sanitized_prop_options(handle, key, options)
57
- cache_key = sanitized_prop_key(handle, key, options)
58
- counter = reader.call(cache_key).to_i
59
-
60
- return counter if disabled?
61
-
62
- if counter >= options[:threshold]
63
- raise Prop::RateLimitExceededError.create(handle, normalize_cache_key(key), options[:threshold], options[:description])
64
- else
65
- writer.call(cache_key, counter + [ 1, options[:increment].to_i ].max)
66
- end
67
- end
68
-
69
- def throttled?(handle, key = nil, options = {})
70
- options = sanitized_prop_options(handle, key, options)
71
- cache_key = sanitized_prop_key(handle, key, options)
72
-
73
- reader.call(cache_key).to_i >= options[:threshold]
74
- end
75
-
76
- def reset(handle, key = nil, options = {})
77
- options = sanitized_prop_options(handle, key, options)
78
- cache_key = sanitized_prop_key(handle, key, options)
79
-
80
- writer.call(cache_key, 0)
81
- end
82
-
83
- def query(handle, key = nil, options = {})
84
- options = sanitized_prop_options(handle, key, options)
85
- cache_key = sanitized_prop_key(handle, key, options)
86
-
87
- reader.call(cache_key).to_i
88
- end
89
- alias :count :query
90
-
91
- private
92
-
93
- # Builds the expiring cache key
94
- def sanitized_prop_key(handle, key, options)
95
- window = (Time.now.to_i / options[:interval])
96
- cache_key = normalize_cache_key([handle, key, window])
97
- "prop/#{Digest::MD5.hexdigest(cache_key)}"
98
- end
99
-
100
- # Sanitizes the option set and sets defaults
101
- def sanitized_prop_options(handle, key, options)
102
- raise RuntimeError.new("No such handle configured: #{handle.inspect}") if handles.nil? || handles[handle].nil?
103
-
104
- defaults = handles[handle]
105
- return {
106
- :key => normalize_cache_key(key),
107
- :increment => defaults[:increment],
108
- :description => defaults[:description],
109
- :threshold => defaults[:threshold].to_i,
110
- :interval => defaults[:interval].to_i
111
- }.merge(options)
112
- end
113
-
114
- # Simple key expansion only supports arrays and primitives
115
- def normalize_cache_key(key)
116
- if key.is_a?(Array)
117
- key.map { |part| normalize_cache_key(part) }.join('/')
118
- else
119
- key.to_s
120
- end
121
- end
122
-
9
+ extend Forwardable
10
+ def_delegators :"Prop::Limiter", :read, :write, :configure, :disabled
11
+ def_delegators :"Prop::Limiter", :throttle!, :throttled?, :count, :query, :reset
123
12
  end
124
13
  end
data/lib/prop/key.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "digest/md5"
2
+
3
+ module Prop
4
+ class Key
5
+
6
+ # Builds the expiring cache key
7
+ def self.build(options)
8
+ key = options.fetch(:key)
9
+ handle = options.fetch(:handle)
10
+ interval = options.fetch(:interval)
11
+
12
+ window = (Time.now.to_i / interval)
13
+ cache_key = normalize([ handle, key, window ])
14
+
15
+ "prop/#{Digest::MD5.hexdigest(cache_key)}"
16
+ end
17
+
18
+ # Simple key expansion only supports arrays and primitives
19
+ def self.normalize(key)
20
+ if key.is_a?(Array)
21
+ key.flatten.join("/")
22
+ else
23
+ key.to_s
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,116 @@
1
+ require 'prop/rate_limited'
2
+ require 'prop/key'
3
+ require 'prop/options'
4
+
5
+ module Prop
6
+ class Limiter
7
+
8
+ class << self
9
+ attr_accessor :handles, :reader, :writer
10
+
11
+ def read(&blk)
12
+ self.reader = blk
13
+ end
14
+
15
+ def write(&blk)
16
+ self.writer = blk
17
+ end
18
+
19
+ # Public: Registers a handle for rate limiting
20
+ #
21
+ # handle - the name of the handle you wish to use in your code, e.g. :login_attempt
22
+ # defaults - the settings for this handle, e.g. { :threshold => 5, :interval => 5.minutes }
23
+ #
24
+ # Raises Prop::RateLimited if the number if the threshold for this handle has been reached
25
+ def configure(handle, defaults)
26
+ raise RuntimeError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
27
+ raise RuntimeError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
28
+
29
+ self.handles ||= {}
30
+ self.handles[handle] = defaults
31
+ end
32
+
33
+ # Public: Disables Prop for a block of code
34
+ #
35
+ # block - a block of code within which Prop will not raise
36
+ def disabled(&block)
37
+ @disabled = true
38
+ yield
39
+ ensure
40
+ @disabled = false
41
+ end
42
+
43
+ # Public: Records a single action for the given handle/key combination.
44
+ #
45
+ # handle - the registered handle associated with the action
46
+ # key - a custom request specific key, e.g. [ account.id, "download", request.remote_ip ]
47
+ # options - request specific overrides to the defaults configured for this handle
48
+ #
49
+ # Raises Prop::RateLimited if the number if the threshold for this handle has been reached
50
+ def throttle!(handle, key = nil, options = {})
51
+ options, cache_key = prepare(handle, key, options)
52
+
53
+ counter = reader.call(cache_key).to_i
54
+
55
+ return counter if disabled?
56
+
57
+ if counter >= options[:threshold]
58
+ raise Prop::RateLimited.new(options.merge(:cache_key => cache_key, :handle => handle))
59
+ else
60
+ writer.call(cache_key, counter + [ 1, options[:increment].to_i ].max)
61
+ end
62
+ end
63
+
64
+ # Public: Allows to query whether the given handle/key combination is currently throttled
65
+ #
66
+ # handle - the throttle identifier
67
+ # key - the associated key
68
+ #
69
+ # Returns true if a call to `throttle!` with same parameters would raise, otherwise false
70
+ def throttled?(handle, key = nil, options = {})
71
+ options, cache_key = prepare(handle, key, options)
72
+ reader.call(cache_key).to_i >= options[:threshold]
73
+ end
74
+
75
+ # Public: Resets a specific throttle
76
+ #
77
+ # handle - the throttle identifier
78
+ # key - the associated key
79
+ #
80
+ # Returns nothing
81
+ def reset(handle, key = nil, options = {})
82
+ options, cache_key = prepare(handle, key, options)
83
+ writer.call(cache_key, 0)
84
+ end
85
+
86
+ # Public: Counts the number of times the given handle/key combination has been hit in the current window
87
+ #
88
+ # handle - the throttle identifier
89
+ # key - the associated key
90
+ #
91
+ # Returns a count of hits in the current window
92
+ def count(handle, key = nil, options = {})
93
+ options, cache_key = prepare(handle, key, options)
94
+ reader.call(cache_key).to_i
95
+ end
96
+ alias :query :count
97
+
98
+ private
99
+
100
+ def disabled?
101
+ !!@disabled
102
+ end
103
+
104
+ def prepare(handle, key, params)
105
+ raise RuntimeError.new("No such handle configured: #{handle.inspect}") unless (handles || {}).key?(handle)
106
+
107
+ defaults = handles[handle]
108
+ options = Prop::Options.build(:key => key, :params => params, :defaults => defaults)
109
+ cache_key = Prop::Key.build(:key => key, :handle => handle, :interval => options[:interval])
110
+
111
+ [ options, cache_key ]
112
+ end
113
+
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,23 @@
1
+ module Prop
2
+ # Convenience middleware that conveys the message configured on a Prop handle as well
3
+ # as time left before the current window has passed in a Retry-After header.
4
+ class Middleware
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env, options = {})
11
+ begin
12
+ @app.call(env)
13
+ rescue Prop::RateLimited => e
14
+ body = e.description || "This action has been rate limited"
15
+
16
+ headers = { "Content-Type" => "text/plain", "Content-Length" => body.size }
17
+ headers["Retry-After"] = e.retry_after if e.retry_after > 0
18
+
19
+ [ 429, headers, [ body ]]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require 'prop/key'
2
+
3
+ module Prop
4
+ class Options
5
+
6
+ # Sanitizes the option set and sets defaults
7
+ def self.build(options)
8
+ key = options.fetch(:key)
9
+ params = options.fetch(:params)
10
+ defaults = options.fetch(:defaults)
11
+ result = defaults.merge(params)
12
+
13
+ result[:key] = Prop::Key.normalize(key)
14
+ result[:threshold] = result[:threshold].to_i
15
+ result[:interval] = result[:interval].to_i
16
+
17
+ raise RuntimeError.new("Invalid threshold setting") unless result[:threshold] > 0
18
+ raise RuntimeError.new("Invalid interval setting") unless result[:interval] > 0
19
+
20
+ result
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module Prop
2
+ class RateLimited < StandardError
3
+ attr_accessor :handle, :cache_key, :retry_after, :description
4
+
5
+ def initialize(options)
6
+ handle = options.fetch(:handle)
7
+ cache_key = options.fetch(:cache_key)
8
+ interval = options.fetch(:interval).to_i
9
+ threshold = options.fetch(:threshold).to_i
10
+
11
+ super("#{handle} threshold of #{threshold}/#{interval}s exceeded for key '#{cache_key}'")
12
+
13
+ self.description = options[:description]
14
+ self.handle = handle
15
+ self.cache_key = cache_key
16
+ self.retry_after = interval - Time.now.to_i % interval
17
+ end
18
+ end
19
+ end
data/prop.gemspec CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'prop'
16
- s.version = '0.6.6'
17
- s.date = '2012-03-31'
16
+ s.version = '0.7.0'
17
+ s.date = '2012-04-06'
18
18
  s.rubyforge_project = 'prop'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -66,9 +66,18 @@ Gem::Specification.new do |s|
66
66
  README.md
67
67
  Rakefile
68
68
  lib/prop.rb
69
+ lib/prop/key.rb
70
+ lib/prop/limiter.rb
71
+ lib/prop/middleware.rb
72
+ lib/prop/options.rb
73
+ lib/prop/rate_limited.rb
69
74
  prop.gemspec
70
75
  test/helper.rb
76
+ test/test_key.rb
77
+ test/test_middleware.rb
78
+ test/test_options.rb
71
79
  test/test_prop.rb
80
+ test/test_rate_limited.rb
72
81
  ]
73
82
  # = MANIFEST =
74
83
 
data/test/test_key.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'helper'
2
+
3
+ class TestKey < Test::Unit::TestCase
4
+
5
+ context Prop::Key do
6
+ context "#build" do
7
+ should "return a hexdigested key" do
8
+ assert_match /prop\/[a-f0-9]+/, Prop::Key.build(:handle => :hello, :key => [ "foo", 2, :bar ], :interval => 60)
9
+ end
10
+ end
11
+
12
+ context "#normalize" do
13
+ should "turn a Fixnum into a String" do
14
+ assert_equal "3", Prop::Key.normalize(3)
15
+ end
16
+
17
+ should "return a String" do
18
+ assert_equal "S", Prop::Key.normalize("S")
19
+ end
20
+
21
+ should "flatten and join an Array" do
22
+ assert_equal "1/B/3", Prop::Key.normalize([ 1, "B", "3" ])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require 'helper'
2
+ require 'prop/middleware'
3
+ require 'prop/rate_limited'
4
+
5
+ class TestMiddleware < Test::Unit::TestCase
6
+
7
+ context Prop::Middleware do
8
+ setup do
9
+ @app = stub()
10
+ @env = {}
11
+ @middleware = Prop::Middleware.new(@app)
12
+ end
13
+
14
+ context "when the app call completes" do
15
+ setup do
16
+ @app.expects(:call).with(@env).returns("response")
17
+ end
18
+
19
+ should "return the response" do
20
+ assert_equal "response", @middleware.call(@env)
21
+ end
22
+ end
23
+
24
+ context "when the app call results in a raised throttle" do
25
+ setup do
26
+ @app.expects(:call).with(@env).raises(Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!"))
27
+ end
28
+
29
+ should "return the rate limited message" do
30
+ response = @middleware.call(@env)
31
+
32
+ assert_equal 429, response[0]
33
+ assert_equal ["Boom!"], response[2]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ require 'helper'
2
+
3
+ class TestOptions < Test::Unit::TestCase
4
+
5
+ context Prop::Options do
6
+ context "#build" do
7
+ setup do
8
+ @args = { :key => "hello", :params => { :foo => "bif" }, :defaults => { :foo => "bar", :baz => "moo", :threshold => 10, :interval => 5 }}
9
+ end
10
+
11
+ context "when given valid input" do
12
+ setup do
13
+ @options = Prop::Options.build(@args)
14
+ end
15
+
16
+ should "support defaults" do
17
+ assert_equal "moo", @options[:baz]
18
+ end
19
+
20
+ should "override defaults" do
21
+ assert_equal "bif", @options[:foo]
22
+ end
23
+ end
24
+
25
+ context "when given invalid input" do
26
+ should "raise when not given an interval" do
27
+ @args[:defaults].delete(:interval)
28
+ assert_raises(RuntimeError) { Prop::Options.build(@args) }
29
+ end
30
+
31
+ should "raise when not given a threshold" do
32
+ @args[:defaults].delete(:threshold)
33
+ assert_raises(RuntimeError) { Prop::Options.build(@args) }
34
+ end
35
+
36
+ should "raise when not given a key" do
37
+ @args.delete(:key)
38
+ assert_raises(IndexError) { Prop::Options.build(@args) }
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
data/test/test_prop.rb CHANGED
@@ -29,7 +29,7 @@ class TestProp < Test::Unit::TestCase
29
29
  assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
30
30
  end
31
31
 
32
- assert_raises(Prop::RateLimitExceededError) { Prop.throttle!(:hello_there, 'some key') }
32
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:hello_there, 'some key') }
33
33
  assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
34
34
  end
35
35
 
@@ -55,9 +55,9 @@ class TestProp < Test::Unit::TestCase
55
55
  Prop.disabled do
56
56
  assert_equal 2, Prop.throttle!(:hello)
57
57
  assert_equal 2, Prop.throttle!(:hello)
58
- assert Prop.disabled?
58
+ assert Prop::Limiter.send(:disabled?)
59
59
  end
60
- assert !Prop.disabled?
60
+ assert !Prop::Limiter.send(:disabled?)
61
61
  assert_equal 3, Prop.throttle!(:hello)
62
62
  end
63
63
  end
@@ -94,7 +94,7 @@ class TestProp < Test::Unit::TestCase
94
94
  end
95
95
  end
96
96
 
97
- context "#query" do
97
+ context "#count" do
98
98
  setup do
99
99
  Prop.configure(:hello, :threshold => 20, :interval => 20)
100
100
  Prop.throttle!(:hello)
@@ -151,22 +151,22 @@ class TestProp < Test::Unit::TestCase
151
151
  assert_equal 50, Prop.query(:hello)
152
152
  end
153
153
 
154
- should "raise Prop::RateLimitExceededError when the threshold is exceeded" do
154
+ should "raise Prop::RateLimited when the threshold is exceeded" do
155
155
  Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
156
156
 
157
157
  5.times do |i|
158
158
  Prop.throttle!(:hello, nil)
159
159
  end
160
- assert_raises(Prop::RateLimitExceededError) do
160
+ assert_raises(Prop::RateLimited) do
161
161
  Prop.throttle!(:hello, nil)
162
162
  end
163
163
 
164
164
  begin
165
165
  Prop.throttle!(:hello, nil)
166
166
  fail
167
- rescue Prop::RateLimitExceededError => e
167
+ rescue Prop::RateLimited => e
168
168
  assert_equal :hello, e.handle
169
- assert_equal "hello threshold of 5 exceeded for key ''", e.message
169
+ assert_match "hello threshold of 5/10s exceeded for key", e.message
170
170
  assert_equal "Boom!", e.description
171
171
  assert e.retry_after
172
172
  end
@@ -0,0 +1,27 @@
1
+ require 'helper'
2
+
3
+ class TestRateLimited < Test::Unit::TestCase
4
+
5
+ context Prop::RateLimited do
6
+ context "#initialize" do
7
+ setup do
8
+ time = Time.at(1333685680)
9
+ Time.stubs(:now).returns(time)
10
+
11
+ @error = Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!")
12
+ end
13
+
14
+ should "return an error instance" do
15
+ assert @error.is_a?(StandardError)
16
+ assert @error.is_a?(Prop::RateLimited)
17
+
18
+ assert_equal "foo", @error.handle
19
+ assert_equal "wibble", @error.cache_key
20
+ assert_equal "Boom!", @error.description
21
+ assert_equal "foo threshold of 10/60s exceeded for key 'wibble'", @error.message
22
+ assert_equal 20, @error.retry_after
23
+ end
24
+ end
25
+
26
+ end
27
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prop
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 3
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 6
9
- - 6
10
- version: 0.6.6
8
+ - 7
9
+ - 0
10
+ version: 0.7.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Morten Primdahl
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-03-31 00:00:00 Z
18
+ date: 2012-04-06 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rake
@@ -88,9 +88,18 @@ files:
88
88
  - README.md
89
89
  - Rakefile
90
90
  - lib/prop.rb
91
+ - lib/prop/key.rb
92
+ - lib/prop/limiter.rb
93
+ - lib/prop/middleware.rb
94
+ - lib/prop/options.rb
95
+ - lib/prop/rate_limited.rb
91
96
  - prop.gemspec
92
97
  - test/helper.rb
98
+ - test/test_key.rb
99
+ - test/test_middleware.rb
100
+ - test/test_options.rb
93
101
  - test/test_prop.rb
102
+ - test/test_rate_limited.rb
94
103
  homepage: http://github.com/morten/prop
95
104
  licenses: []
96
105
 
@@ -125,5 +134,9 @@ signing_key:
125
134
  specification_version: 2
126
135
  summary: Gem for implementing rate limits.
127
136
  test_files:
137
+ - test/test_key.rb
138
+ - test/test_middleware.rb
139
+ - test/test_options.rb
128
140
  - test/test_prop.rb
141
+ - test/test_rate_limited.rb
129
142
  has_rdoc: