Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactor Twitter::RateLimit class to be non-global

The global data being overwritten when there were multiple clients
running in parallel. Now, each client has its own Twitter::RateLimit
instance, as does each Twitter::Error object.

Closes #289.
  • Loading branch information...
commit 6e9da0d0b8ae61e077eb631514922635a78951a7 1 parent 1702f05
@sferik authored
View
2  README.md
@@ -162,7 +162,7 @@ Here are some fun facts about the 3.0 release:
* The entire library is implemented in just 2,254 lines of code
* With over 5,000 lines of specs, the spec-to-code ratio is well over 2:1
-* The spec suite contains 653 examples and runs in under 2 seconds on a MacBook
+* The spec suite contains 658 examples and runs in under 2 seconds on a MacBook
* This project has 100% C0 code coverage (the tests execute every line of
source code at least once)
* At the time of release, this library is comprehensive: you can request all
View
7 lib/twitter.rb
@@ -10,9 +10,14 @@ class << self
#
# @return [Twitter::Client]
def client
- Twitter::Client.new(options)
+ if @client && @client.options == self.options
+ @client
+ else
+ @client = Twitter::Client.new(options)
+ end
end
+ # @return [Hash]
def options
@options = {}
Twitter::Configurable.keys.each do |key|
View
4 lib/twitter/base.rb
@@ -1,7 +1,6 @@
module Twitter
class Base
attr_reader :attrs
- alias body attrs
alias to_hash attrs
# Define methods that retrieve the value from an initialized instance variable Hash, using the attribute as a key
@@ -74,7 +73,7 @@ def self.fetch_or_new(attrs={})
# @param attrs [Hash]
# @return [Twitter::Base]
def initialize(attrs={})
- self.update(attrs)
+ @attrs = attrs
end
# Fetches an attribute of an object using hash notation
@@ -91,7 +90,6 @@ def [](method)
# @param attrs [Hash]
# @return [Twitter::Base]
def update(attrs)
- @attrs ||= {}
@attrs.update(attrs)
self
end
View
16 lib/twitter/client.rb
@@ -15,6 +15,7 @@
require 'twitter/metadata'
require 'twitter/oembed'
require 'twitter/place'
+require 'twitter/rate_limit'
require 'twitter/rate_limit_status'
require 'twitter/relationship'
require 'twitter/saved_search'
@@ -35,6 +36,7 @@ module Twitter
# @see http://dev.twitter.com/pages/every_developer
class Client
include Twitter::Configurable
+ attr_reader :rate_limit
MAX_USERS_PER_REQUEST = 100
METHOD_RATE_LIMITED = {
:accept => false,
@@ -173,6 +175,16 @@ def initialize(options={})
Twitter::Configurable.keys.each do |key|
instance_variable_set("@#{key}", options[key] || Twitter.options[key])
end
+ @rate_limit = Twitter::RateLimit.new
+ end
+
+ # @return [Hash]
+ def options
+ @options = {}
+ Twitter::Configurable.keys.each do |key|
+ @options[key] = instance_variable_get("@#{key}")
+ end
+ @options
end
# Check whether a method is rate limited
@@ -2876,7 +2888,7 @@ def request(method, path, params, options)
request_headers[:authorization] = authorization.to_s
end
connection.url_prefix = options[:endpoint] || @endpoint
- connection.run_request(method.to_sym, path, nil, request_headers) do |request|
+ response = connection.run_request(method.to_sym, path, nil, request_headers) do |request|
unless params.empty?
case request.method
when :post, :put
@@ -2887,6 +2899,8 @@ def request(method, path, params, options)
end
yield request if block_given?
end.env
+ @rate_limit.update(response[:response_headers])
+ response
rescue Faraday::Error::ClientError
raise Twitter::Error::ClientError
end
View
14 lib/twitter/cursor.rb
@@ -19,9 +19,9 @@ def self.from_response(response={}, method=:ids, klass=nil)
# Initializes a new Cursor
#
# @param attrs [Hash]
- # @return [Twitter::Base]
+ # @return [Twitter::Cursor]
def initialize(attrs={}, method=:ids, klass=nil)
- self.update(attrs)
+ @attrs = attrs
@collection = Array(attrs[method.to_sym]).map do |item|
if klass
klass.fetch_or_new(item)
@@ -56,15 +56,5 @@ def last?
end
alias last last?
- # Update the attributes of an object
- #
- # @param attrs [Hash]
- # @return [Twitter::Cursor]
- def update(attrs)
- @attrs ||= {}
- @attrs.update(attrs)
- self
- end
-
end
end
View
2  lib/twitter/default.rb
@@ -5,7 +5,6 @@
require 'twitter/response/parse_json'
require 'twitter/response/raise_client_error'
require 'twitter/response/raise_server_error'
-require 'twitter/response/rate_limit'
require 'twitter/version'
module Twitter
@@ -58,7 +57,6 @@ def middleware
builder.use Twitter::Response::RaiseClientError # Handle 4xx server responses
builder.use Twitter::Response::ParseJson # Parse JSON response bodies using MultiJson
builder.use Twitter::Response::RaiseServerError # Handle 5xx server responses
- builder.use Twitter::Response::RateLimit # Update RateLimit object
builder.adapter Faraday.default_adapter # Set Faraday's HTTP adapter
end
)
View
5 lib/twitter/error.rb
@@ -1,7 +1,7 @@
module Twitter
# Custom error class for rescuing from all Twitter errors
class Error < StandardError
- attr_reader :wrapped_exception
+ attr_reader :rate_limit, :wrapped_exception
def self.errors
@errors ||= Hash[descendants.map{|klass| [klass.const_get(:HTTP_STATUS_CODE), klass]}]
@@ -15,7 +15,8 @@ def self.descendants
#
# @param exception [Exception, String]
# @return [Twitter::Error]
- def initialize(exception=$!)
+ def initialize(exception=$!, response_headers={})
+ @rate_limit = Twitter::RateLimit.new(response_headers)
if exception.respond_to?(:backtrace)
super(exception.message)
@wrapped_exception = exception
View
4 lib/twitter/error/client_error.rb
@@ -9,8 +9,8 @@ class ClientError < Twitter::Error
#
# @param body [Hash]
# @return [Twitter::Error]
- def self.from_response_body(body)
- new(parse_error(body))
+ def self.from_response(response={})
+ new(parse_error(response[:body]), response[:response_headers])
end
private
View
12 lib/twitter/error/server_error.rb
@@ -6,12 +6,20 @@ class Error
class ServerError < Twitter::Error
MESSAGE = "Server Error"
+ # Create a new error from an HTTP environment
+ #
+ # @param body [Hash]
+ # @return [Twitter::Error]
+ def self.from_response(response={})
+ new(nil, response[:response_headers])
+ end
+
# Initializes a new ServerError object
#
# @param message [String]
# @return [Twitter::Error::ServerError]
- def initialize(message=nil)
- super(message || self.class.const_get(:MESSAGE))
+ def initialize(message=nil, response_headers={})
+ super((message || self.class.const_get(:MESSAGE)), response_headers)
end
end
View
6 lib/twitter/identity.rb
@@ -19,7 +19,7 @@ def self.fetch(attrs)
# Stores an object in the identity map.
#
# @param attrs [Hash]
- # @return [Twitter::Base]
+ # @return [Twitter::Identity]
def self.store(object)
Twitter.identity_map[self] ||= {}
object.id && Twitter.identity_map[self][object.id] = object || super(object)
@@ -29,9 +29,9 @@ def self.store(object)
#
# @param attrs [Hash]
# @raise [ArgumentError] Error raised when supplied argument is missing an :id key.
- # @return [Twitter::Base]
+ # @return [Twitter::Identity]
def initialize(attrs={})
- self.update(attrs)
+ super
raise ArgumentError, "argument must have an :id key" unless self.id
end
View
40 lib/twitter/rate_limit.rb
@@ -1,47 +1,45 @@
-require 'singleton'
-
module Twitter
class RateLimit
- include Singleton
- attr_accessor :response_headers
- alias headers response_headers
+ attr_reader :attrs
+ alias to_hash attrs
+
+ # @deprecated This method exists to provide backwards compatibility to when
+ # Twitter::RateLimit was a singleton. Safe to remove in version 4.
+ def self.instance
+ Twitter.rate_limit
+ end
- def initialize
- self.reset!
+ # @return [Twitter::RateLimit]
+ def initialize(attrs={})
+ @attrs = attrs
end
# @return [String]
def class
- @response_headers.values_at('x-ratelimit-class', 'X-RateLimit-Class').compact.first
+ @attrs.values_at('x-ratelimit-class', 'X-RateLimit-Class').compact.first
end
# @return [Integer]
def limit
- limit = @response_headers.values_at('x-ratelimit-limit', 'X-RateLimit-Limit').compact.first
+ limit = @attrs.values_at('x-ratelimit-limit', 'X-RateLimit-Limit').compact.first
limit.to_i if limit
end
# @return [Integer]
def remaining
- remaining = @response_headers.values_at('x-ratelimit-remaining', 'X-RateLimit-Remaining').compact.first
+ remaining = @attrs.values_at('x-ratelimit-remaining', 'X-RateLimit-Remaining').compact.first
remaining.to_i if remaining
end
- # @return [Twitter::RateLimit]
- def reset!
- @response_headers = {}
- self
- end
-
# @return [Time]
def reset_at
- reset = @response_headers.values_at('x-ratelimit-reset', 'X-RateLimit-Reset').compact.first
+ reset = @attrs.values_at('x-ratelimit-reset', 'X-RateLimit-Reset').compact.first
Time.at(reset.to_i) if reset
end
# @return [Integer]
def reset_in
- if retry_after = @response_headers.values_at('retry-after', 'Retry-After').compact.first
+ if retry_after = @attrs.values_at('retry-after', 'Retry-After').compact.first
retry_after.to_i
elsif reset_at
[(reset_at - Time.now).ceil, 0].max
@@ -51,10 +49,10 @@ def reset_in
# Update the attributes of a Relationship
#
- # @param response_headers [Hash]
+ # @param attrs [Hash]
# @return [Twitter::RateLimit]
- def update(response_headers)
- @response_headers.update(response_headers)
+ def update(attrs)
+ @attrs.update(attrs)
self
end
View
9 lib/twitter/relationship.rb
@@ -5,6 +5,14 @@
module Twitter
class Relationship < Twitter::Base
+ # Initializes a new object
+ #
+ # @param attrs [Hash]
+ # @return [Twitter::Relationship]
+ def initialize(attrs={})
+ @attrs = attrs[:relationship]
+ end
+
# @return [Twitter::User]
def source
@source ||= Twitter::SourceUser.fetch_or_new(@attrs[:source]) unless @attrs[:source].nil?
@@ -20,7 +28,6 @@ def target
# @param attrs [Hash]
# @return [Twitter::Relationship]
def update(attrs)
- @attrs ||= {}
@attrs.update(attrs[:relationship]) unless attrs[:relationship].nil?
self
end
View
2  lib/twitter/response/raise_client_error.rb
@@ -13,7 +13,7 @@ class RaiseClientError < Faraday::Response::Middleware
def on_complete(env)
status_code = env[:status].to_i
error_class = Twitter::Error::ClientError.errors[status_code]
- raise error_class.from_response_body(env[:body]) if error_class
+ raise error_class.from_response(env) if error_class
end
end
View
2  lib/twitter/response/raise_server_error.rb
@@ -10,7 +10,7 @@ class RaiseServerError < Faraday::Response::Middleware
def on_complete(env)
status_code = env[:status].to_i
error_class = Twitter::Error::ServerError.errors[status_code]
- raise error_class.new if error_class
+ raise error_class.from_response(env) if error_class
end
end
View
14 lib/twitter/response/rate_limit.rb
@@ -1,14 +0,0 @@
-require 'faraday'
-require 'twitter/rate_limit'
-
-module Twitter
- module Response
- class RateLimit < Faraday::Response::Middleware
-
- def on_complete(env)
- Twitter::RateLimit.instance.update(env[:response_headers])
- end
-
- end
- end
-end
View
8 spec/twitter/client_spec.rb
@@ -83,6 +83,14 @@
client2.verify_credentials.screen_name.should eq 'pengwynn'
end
+ describe "#initalize" do
+ it "returns a different rate limit object for a new client" do
+ client1 = Twitter::Client.new
+ client2 = Twitter::Client.new
+ client1.rate_limit.should_not eq client2.rate_limit
+ end
+ end
+
describe "#rate_limited?" do
before do
@client = Twitter::Client.new
View
32 spec/twitter/rate_limit_spec.rb
@@ -1,54 +1,58 @@
require 'helper'
describe Twitter::RateLimit do
- after do
- Twitter::RateLimit.instance.reset!
+
+ describe ".instance" do
+ it "delegates to Twitter.rate_limit" do
+ rate_limit = Twitter::RateLimit.instance
+ rate_limit.should eq Twitter.rate_limit
+ end
end
describe "#limit" do
it "returns an Integer when X-RateLimit-Limit header is set" do
- rate_limit = Twitter::RateLimit.instance.update('X-RateLimit-Limit' => "150")
+ rate_limit = Twitter::RateLimit.new('X-RateLimit-Limit' => "150")
rate_limit.limit.should be_an Integer
rate_limit.limit.should eq 150
end
it "returns nil when X-RateLimit-Limit header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.limit.should be_nil
end
end
describe "#class" do
it "returns a String when X-RateLimit-Class header is set" do
- rate_limit = Twitter::RateLimit.instance.update('X-RateLimit-Class' => "api")
+ rate_limit = Twitter::RateLimit.new('X-RateLimit-Class' => "api")
rate_limit.class.should be_an String
rate_limit.class.should eq "api"
end
it "returns nil when X-RateLimit-Class header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.class.should be_nil
end
end
describe "#remaining" do
it "returns an Integer when X-RateLimit-Remaining header is set" do
- rate_limit = Twitter::RateLimit.instance.update('X-RateLimit-Remaining' => "149")
+ rate_limit = Twitter::RateLimit.new('X-RateLimit-Remaining' => "149")
rate_limit.remaining.should be_an Integer
rate_limit.remaining.should eq 149
end
it "returns nil when X-RateLimit-Remaining header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.remaining.should be_nil
end
end
describe "#reset_at" do
it "returns a Time when X-RateLimit-Reset header is set" do
- rate_limit = Twitter::RateLimit.instance.update('X-RateLimit-Reset' => "1339019097")
+ rate_limit = Twitter::RateLimit.new('X-RateLimit-Reset' => "1339019097")
rate_limit.reset_at.should be_a Time
rate_limit.reset_at.should eq Time.at(1339019097)
end
it "returns nil when X-RateLimit-Reset header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.reset_at.should be_nil
end
end
@@ -61,24 +65,24 @@
Timecop.return
end
it "returns an Integer when X-RateLimit-Reset header is set" do
- rate_limit = Twitter::RateLimit.instance.update('X-RateLimit-Reset' => "1339019097")
+ rate_limit = Twitter::RateLimit.new('X-RateLimit-Reset' => "1339019097")
rate_limit.reset_in.should be_an Integer
rate_limit.reset_in.should eq 15777
end
it "returns nil when X-RateLimit-Reset header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.reset_in.should be_nil
end
end
describe "#retry_after" do
it "returns an Integer when Retry-After header is set" do
- rate_limit = Twitter::RateLimit.instance.update('Retry-After' => "1339019097")
+ rate_limit = Twitter::RateLimit.new('Retry-After' => "1339019097")
rate_limit.retry_after.should be_an Integer
rate_limit.retry_after.should eq 1339019097
end
it "returns nil when Retry-After header is not set" do
- rate_limit = Twitter::RateLimit.instance
+ rate_limit = Twitter::RateLimit.new
rate_limit.retry_after.should be_nil
end
end
View
12 spec/twitter/relationship_spec.rb
@@ -8,7 +8,7 @@
source.should be_a Twitter::SourceUser
end
it "returns nil when source is not set" do
- source = Twitter::Relationship.new.source
+ source = Twitter::Relationship.new(:relationship => {}).source
source.should be_nil
end
end
@@ -19,9 +19,17 @@
target.should be_a Twitter::TargetUser
end
it "returns nil when target is not set" do
- target = Twitter::Relationship.new.target
+ target = Twitter::Relationship.new(:relationship => {}).target
target.should be_nil
end
end
+ describe "#update" do
+ it "updates a relationship" do
+ relationship = Twitter::Relationship.new(:relationship => {:target => {:id => 7505382}})
+ relationship.update(:relationship => {:target => {:id => 14100886}})
+ relationship.target.id.should eq 14100886
+ end
+ end
+
end
View
17 spec/twitter_spec.rb
@@ -40,6 +40,23 @@
it "returns a Twitter::Client" do
Twitter.client.should be_a Twitter::Client
end
+
+ context "when the options don't change" do
+ it "caches the client" do
+ Twitter.client.should eq Twitter.client
+ end
+ end
+ context "when the options change" do
+ it "busts the cache" do
+ client1 = Twitter.client
+ Twitter.configure do |config|
+ config.consumer_key = 'abc'
+ config.consumer_secret = '123'
+ end
+ client2 = Twitter.client
+ client1.should_not eq client2
+ end
+ end
end
describe ".configure" do
Please sign in to comment.
Something went wrong with that request. Please try again.