rate-limiting 1.0.2

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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rate-limiting.gemspec
4
+ gemspec
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rate_limiting'
@@ -0,0 +1,5 @@
1
+ module Rate
2
+ module Limiting
3
+ VERSION = "1.0.2"
4
+ end
5
+ end
@@ -0,0 +1,125 @@
1
+ require "json"
2
+ require "rule"
3
+
4
+ class RateLimiting
5
+
6
+ def initialize(app, &block)
7
+ @app = app
8
+ @rules = []
9
+ @cache = {}
10
+ block.call(self)
11
+ end
12
+
13
+ def call(env)
14
+ request = Rack::Request.new(env)
15
+ (limit_header = allowed?(request)) ? respond(env, limit_header) : rate_limit_exceeded(env['HTTP_ACCEPT'])
16
+ end
17
+
18
+ def respond(env, limit_header)
19
+ status, header, response = @app.call(env)
20
+ (limit_header.class == Hash) ? [status, header.merge(limit_header), response] : [status, header, response]
21
+ end
22
+
23
+ def rate_limit_exceeded(accept)
24
+ case accept.gsub(/;.*/, "").split(',')[0]
25
+ when "text/xml" then message, type = xml_error("403", "Rate Limit Exceeded"), "text/xml"
26
+ when "application/json" then message, type = ["Rate Limit Exceeded"].to_json, "application/json"
27
+ else
28
+ message, type = ["Rate Limit Exceeded"], "text/html"
29
+ end
30
+ [403, {"Content-Type" => type}, message]
31
+ end
32
+
33
+ def define_rule(options)
34
+ @rules << Rule.new(options)
35
+ end
36
+
37
+ def set_cache(cache)
38
+ @cache = cache
39
+ end
40
+
41
+ def cache
42
+ case @cache
43
+ when Proc then @cache.call
44
+ else @cache
45
+ end
46
+ end
47
+
48
+ def cache_has?(key)
49
+ case
50
+ when cache.respond_to?(:has_key?)
51
+ cache.has_key?(key)
52
+ when cache.respond_to?(:get)
53
+ cache.get(key) rescue false
54
+ else false
55
+ end
56
+ end
57
+
58
+ def cache_get(key)
59
+ case
60
+ when cache.respond_to?(:[])
61
+ return cache[key]
62
+ when cache.respond_to?(:get)
63
+ return cache.get(key) || nil
64
+ end
65
+ end
66
+
67
+ def cache_set(key, value)
68
+ case
69
+ when cache.respond_to?(:[])
70
+ begin
71
+ cache[key] = value
72
+ rescue TypeError => e
73
+ cache[key] = value.to_s
74
+ end
75
+ when cache.respond_to?(:set)
76
+ cache.set(key, value)
77
+ end
78
+ end
79
+
80
+ def allowed?(request)
81
+ if rule = find_matching_rule(request)
82
+ apply_rule(request, rule)
83
+ else
84
+ true
85
+ end
86
+ end
87
+
88
+ def find_matching_rule(request)
89
+ @rules.each do |rule|
90
+ return rule if request.path =~ rule.match
91
+ end
92
+ nil
93
+ end
94
+
95
+ def apply_rule(request, rule)
96
+ key = rule.get_key(request)
97
+ if cache_has?(key)
98
+ record = cache_get(key)
99
+ if (reset = record.split(':')[1]) > Time.now.strftime("%d%m%y%H%M%S")
100
+ if (times = record.split(':')[0].to_i) < rule.limit
101
+ response = get_header(times + 1, reset, rule.limit)
102
+ record = record.gsub(/.*:/, "#{times + 1}:")
103
+ else
104
+ return false
105
+ end
106
+ else
107
+ response = get_header(1, reset = rule.get_expiration, rule.limit)
108
+ cache_set(key, "1:" + rule.get_expiration)
109
+ end
110
+ else
111
+ response = get_header(1, reset = rule.get_expiration, rule.limit)
112
+ cache_set(key, "1:" + rule.get_expiration)
113
+ end
114
+ response
115
+ end
116
+
117
+ def get_header(times, reset, limit)
118
+ {'x-RateLimit-Limit' => limit.to_s, 'x-RateLimit-Remaining' => (limit - times).to_s, 'x-RateLimit-Reset' => reset.to_s }
119
+ end
120
+
121
+ def xml_error(code, message)
122
+ "<?xml version=\"1.0\"?>\n<error>\n <code>#{code}</code>\n <message>#{message}</message>\n</error>"
123
+ end
124
+
125
+ end
@@ -0,0 +1,57 @@
1
+ class Rule
2
+
3
+ def initialize(options)
4
+ default_options = {
5
+ :match => /.*/,
6
+ :metric => :rph,
7
+ :type => :frequency,
8
+ :limit => 100,
9
+ :per_ip => true,
10
+ :token => false
11
+ }
12
+ @options = default_options.merge(options)
13
+
14
+ end
15
+
16
+ def match
17
+ @options[:match].class == String ? Regexp.new(@options[:match] + "$") : @options[:match]
18
+ end
19
+
20
+ def limit
21
+ (@options[:type] == :frequency ? 1 : @options[:limit])
22
+ end
23
+
24
+ def get_expiration
25
+ (Time.now + ( @options[:type] == :frequency ? get_frequency : get_fixed )).strftime("%d%m%y%H%M%S")
26
+ end
27
+
28
+ def get_frequency
29
+ case @options[:metric]
30
+ when :rpd
31
+ return (86400/@options[:limit] == 0 ? 1 : 86400/@options[:limit])
32
+ when :rph
33
+ return (3600/@options[:limit] == 0 ? 1 : 3600/@options[:limit])
34
+ when :rpm
35
+ return (60/@options[:limit] == 0 ? 1 : 60/@options[:limit])
36
+ end
37
+ end
38
+
39
+ def get_fixed
40
+ case @options[:metric]
41
+ when :rpd
42
+ return 86400
43
+ when :rph
44
+ return 3600
45
+ when :rpm
46
+ return 60
47
+ end
48
+ end
49
+
50
+ def get_key(request)
51
+ key = request.path
52
+ key = key + request.ip.to_s if @options[:per_ip]
53
+ key = key + request.params[@options[:token].to_s] if @options[:token]
54
+ key
55
+ end
56
+ end
57
+
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rate-limiting/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rate-limiting"
7
+ s.version = Rate::Limiting::VERSION
8
+ s.authors = ["alepaez, pnegri"]
9
+ s.email = ["alexandre@iugu.com.br"]
10
+ s.homepage = "https://github.com/iugu/rate-limiting"
11
+ s.summary = %q{Rack Rate-Limit Gem}
12
+ s.description = %q{Easy way to Rate Limit your Rack app}
13
+
14
+ s.rubyforge_project = "rate-limiting"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rack-test"
27
+ s.add_dependency "json"
28
+
29
+ end
@@ -0,0 +1,50 @@
1
+ Rate Limiting
2
+ ===============
3
+
4
+
5
+ How to use it
6
+ ----------------
7
+
8
+ **Adding to Rails 3.x**
9
+
10
+ \# config/application.rb
11
+
12
+ > class Application < Rails::Application
13
+ >
14
+ > config.middleware.use RateLimiting do |r|
15
+ >
16
+ > r.define_rule( :match => '/resource', :type => :fixed, :metric => :rph, :limit => 300 )
17
+ >
18
+ > end
19
+ >
20
+ > end
21
+
22
+ Rule Options
23
+ ----------------
24
+
25
+ **match**
26
+
27
+ Accepts aimed resource path or Regexp like '/resource' or "/resource/.*"
28
+
29
+ **metric**
30
+
31
+ :rpd - Requests per Day
32
+
33
+ :rph - Requests per Hour
34
+
35
+ :rpm - Requests per Minute
36
+
37
+ **type**
38
+
39
+ :frequency - 1 request per (time/limit)
40
+
41
+ :fixed - limit requests per time
42
+
43
+ **token**
44
+
45
+ :foo - limit by request parameter 'foo'
46
+
47
+ **per_ip**
48
+
49
+ Boolean, true = limit by IP
50
+
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rpd rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rpd', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rpd', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rph rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rph', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rph', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fixed/rpm rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed limit' do
7
+ get '/fixed/rpm', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed limit' do
12
+ 2.times { get '/fixed/rpm', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rpd rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rpd', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rpd', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rph rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rph', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rph', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Frequency/rpm rule request" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should be allowed if not exceed 1 request per min' do
7
+ get '/freq/rpm', {}, {'HTTP_ACCEPT' => "text/html"}
8
+ last_response.body.should show_allowed_response
9
+ end
10
+
11
+ it 'should not be allowed if exceed 1 request per min' do
12
+ 2.times { get '/freq/rpm', {}, {'HTTP_ACCEPT' => "text/html"} }
13
+ last_response.body.should show_not_allowed_response
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ describe "response headers" do
4
+ include Rack::Test::Methods
5
+
6
+ context "limited request" do
7
+ before(:each) do
8
+ get '/header', {}, {'HTTP_ACCEPT' => 'text/html'}
9
+ end
10
+
11
+ it 'should have x-RateLimit-Limit' do
12
+ last_response.header.should include "x-RateLimit-Limit"
13
+ end
14
+
15
+ it 'should have x-RateLimit-Remaining' do
16
+ last_response.header.should include "x-RateLimit-Remaining"
17
+ end
18
+
19
+ it 'should have x-RateLimit-Reset' do
20
+ last_response.header.should include "x-RateLimit-Reset"
21
+ end
22
+
23
+ it 'should have the right limit' do
24
+ last_response.header['x-RateLimit-Limit'].should == 1
25
+ end
26
+
27
+ it 'should have the right remaining' do
28
+ last_response.header['x-RateLimit-Remaining'].should == 0
29
+ end
30
+
31
+ end
32
+
33
+ context "not limited request" do
34
+
35
+ before(:each) do
36
+ get '/not_limited'
37
+ end
38
+
39
+ it 'should have x-RateLimit-Limit' do
40
+ last_response.header.should_not include "x-RateLimit-Limit"
41
+ end
42
+
43
+ it 'should have x-RateLimit-Remaining' do
44
+ last_response.header.should_not include "x-RateLimit-Remaining"
45
+ end
46
+
47
+ it 'should have x-RateLimit-Reset' do
48
+ last_response.header.should_not include "x-RateLimit-Reset"
49
+ end
50
+
51
+ end
52
+
53
+
54
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe "html request" do
4
+
5
+ include Rack::Test::Methods
6
+
7
+ it 'should receive allowed' do
8
+ app.should_receive(:allowed?).twice
9
+ get '/test', {}, {'HTTP_ACCEPT' => "text/html"}
10
+ get '/test2', {}, {'HTTP_ACCEPT' => "text/html"}
11
+ end
12
+
13
+ it 'should receive allowed' do
14
+ 2.times { get '/html', {}, {'HTTP_ACCEPT' => "text/html"} }
15
+ last_response.content_type.should == "text/html"
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe "json request" do
4
+ include Rack::Test::Methods
5
+ it 'should receive allowed' do
6
+ 2.times { get '/json', {}, {'HTTP_ACCEPT' => "application/json"} }
7
+ last_response.content_type.should == "application/json"
8
+ end
9
+ end
10
+
@@ -0,0 +1,63 @@
1
+ require 'rspec'
2
+ require 'rack/test'
3
+ require 'rate_limiting'
4
+
5
+ def test_app
6
+ @test_app ||= mock("Test Rack App")
7
+ @test_app.stub!(:call).with(anything()).and_return([200, {}, "Test App Body"])
8
+ @test_app
9
+ end
10
+
11
+ def app
12
+ @test_app ||= test_app
13
+ @app ||= RateLimiting.new(@test_app) do |r|
14
+ r.define_rule(:match => '/html', :limit => 1)
15
+ r.define_rule(:match => '/json', :metric => :rph, :type => :frequency, :limit => 60)
16
+ r.define_rule(:match => '/xml', :metric => :rph, :type => :frequency, :limit => 60)
17
+ r.define_rule(:match => '/token/ip', :limit => 1, :token => :id, :per_ip => true)
18
+ r.define_rule(:match => '/token', :limit => 1, :token => :id, :per_ip => false)
19
+ r.define_rule(:match => '/fixed/rpm', :metric => :rpm, :type => :fixed, :limit => 1)
20
+ r.define_rule(:match => '/fixed/rph', :metric => :rph, :type => :fixed, :limit => 1)
21
+ r.define_rule(:match => '/fixed/rpd', :metric => :rpd, :type => :fixed, :limit => 1)
22
+ r.define_rule(:match => '/freq/rpm', :metric => :rpm, :type => :frequency, :limit => 1)
23
+ r.define_rule(:match => '/freq/rph', :metric => :rph, :type => :frequency, :limit => 60)
24
+ r.define_rule(:match => '/freq/rpd', :metric => :rpd, :type => :frequency, :limit => 1440)
25
+ r.define_rule(:match => '/header', :metric => :rph, :type => :frequency, :limit => 60)
26
+ end
27
+ end
28
+
29
+ Spec::Matchers.define :show_allowed_response do
30
+ match do |body|
31
+ body.include?("Test App Body")
32
+ end
33
+
34
+ failure_message_for_should do
35
+ "expected response to show the allowed response"
36
+ end
37
+
38
+ failure_message_for_should_not do
39
+ "expected response not to show the allowed response"
40
+ end
41
+
42
+ description do
43
+ "expected the allowed response"
44
+ end
45
+ end
46
+
47
+ Spec::Matchers.define :show_not_allowed_response do
48
+ match do |body|
49
+ body.include?("Rate Limit Exceeded")
50
+ end
51
+
52
+ failure_message_for_should do
53
+ "expected response to show the not allowed response"
54
+ end
55
+
56
+ failure_message_for_should_not do
57
+ "expected response not to show the not allowed response"
58
+ end
59
+
60
+ description do
61
+ "expected the not allowed response"
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe "defined token rule" do
4
+ include Rack::Test::Methods
5
+
6
+ it 'should allow diferent ids' do
7
+ get '/token', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"}
8
+ get '/token', { :id => "2" }, {'HTTP_ACCEPT' => "text/html"}
9
+ last_response.body.should show_allowed_response
10
+ end
11
+
12
+ it 'should not allow equal ids' do
13
+ 2.times { get '/token', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"} }
14
+ last_response.body.should show_not_allowed_response
15
+ end
16
+
17
+ context "+ per_ip" do
18
+
19
+ it 'should allow diferent ids' do
20
+ get '/token/ip', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"}
21
+ get '/token/ip', { :id => "2" }, {'HTTP_ACCEPT' => "text/html"}
22
+ last_response.body.should show_allowed_response
23
+ end
24
+
25
+ it 'should not allow equal ids' do
26
+ 2.times { get '/token/ip', { :id => "1" }, {'HTTP_ACCEPT' => "text/html"} }
27
+ last_response.body.should show_not_allowed_response
28
+ end
29
+
30
+
31
+ end
32
+
33
+ end
34
+
35
+
36
+
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe "xml request" do
4
+
5
+ include Rack::Test::Methods
6
+
7
+ it 'should receive allowed' do
8
+ 2.times { get '/xml', {}, {'HTTP_ACCEPT' => "text/xml"} }
9
+ last_response.content_type.should == "text/xml"
10
+ end
11
+
12
+ end
13
+
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rate-limiting
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - alepaez, pnegri
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rack-test
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Easy way to Rate Limit your Rack app
63
+ email:
64
+ - alexandre@iugu.com.br
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .rspec
71
+ - Gemfile
72
+ - Rakefile
73
+ - init.rb
74
+ - lib/rate-limiting/version.rb
75
+ - lib/rate_limiting.rb
76
+ - lib/rule.rb
77
+ - rate-limiting.gemspec
78
+ - readme.md
79
+ - spec/fixed/rpd_spec.rb
80
+ - spec/fixed/rph_spec.rb
81
+ - spec/fixed/rpm_spec.rb
82
+ - spec/frequency/rpd_spec.rb
83
+ - spec/frequency/rph_spec.rb
84
+ - spec/frequency/rpm_spec.rb
85
+ - spec/headers_spec.rb
86
+ - spec/html_request_spec.rb
87
+ - spec/json_request_spec.rb
88
+ - spec/spec_helper.rb
89
+ - spec/token_spec.rb
90
+ - spec/xml_request_spec.rb
91
+ homepage: https://github.com/iugu/rate-limiting
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project: rate-limiting
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Rack Rate-Limit Gem
115
+ test_files:
116
+ - spec/fixed/rpd_spec.rb
117
+ - spec/fixed/rph_spec.rb
118
+ - spec/fixed/rpm_spec.rb
119
+ - spec/frequency/rpd_spec.rb
120
+ - spec/frequency/rph_spec.rb
121
+ - spec/frequency/rpm_spec.rb
122
+ - spec/headers_spec.rb
123
+ - spec/html_request_spec.rb
124
+ - spec/json_request_spec.rb
125
+ - spec/spec_helper.rb
126
+ - spec/token_spec.rb
127
+ - spec/xml_request_spec.rb