November 07, 2010

#22 Code Reading - The Twitter Gem

Just recently, the release candidate for the 1.0 version of the Twitter gem became available. Apparently this version is a major overhaul of previous versions. In our never ending quest to become better programmers, we'll read through some of the Twitter gem code before using it.

Configuration

Looking at the README file, we see how to setup the Twitter configuration. What's going on when we put this code into our app? If we open configuration.rb, we see the Configuration module. At the top, a number of constants are set. For VALID_OPTIONS_KEYS, getter and setter instance methods are created via attr_accessor. (The splat operator ' * ' is used to expand VALID_OPTIONS_KEYS so that each array member is passed to attr_accessor.)

So where is the Configuration module used? If we look in twitter.rb, we see that it is extended into the base Twitter module. Because we extend the module, the instance methods defined in the Configuration module become class/module methods in the base Twitter module. Now we can call 'Twitter.consumer_key'. But instead, we make use of the convienence method 'configure'. This method simply yields 'self' (which in this context is the base Twitter module) to the supplied block so that it can be assigned to a temporary variable 'config' and other configuration parameters can be set.

In the Configuration module, there is a method 'self.extended(base)' that is defined. This is a callback/hook similar to 'self.included()' which we saw in Post #16. So, when the base Twitter module extends Configuration, this extended() method will automatically be called and some of the configuration settings will be reset to their default values.
README.mkd, lib/twitter/configuration.rb and lib/twitter.rb
# README.mkd

Twitter.configure do |config|
  config.consumer_key = YOUR_CONSUMER_KEY
  config.consumer_secret = YOUR_CONSUMER_SECRET
  ...
end
________________________________________

# configuration.rb

module Twitter
  module Configuration
    VALID_OPTIONS_KEYS = [:consumer_key, ...]
    ...
    attr_accessor *VALID_OPTIONS_KEYS
    
    def self.extended(base)
      base.reset
    end

    def configure
      yield self
    end

    def options
      VALID_OPTIONS_KEYS.inject({}){|o, k| o.merge!(k => send(k))}
    end
    
    def reset
      self.adapter = DEFAULT_ADAPTER
      ...
    end
________________________________________

# twitter.rb

module Twitter
  extend Configuration

The Base Twitter Module

Looking at the other methods in the base Twitter module, we see that this module is basically a delagator to the Twitter::Client module. This is for convience. So instead of writing 'Twitter::Client.new', we can just write 'Twitter.client'.

The module takes advantage of method_missing. If we try to call a method that is not defined in the base Twitter module, then method_missing is invoked. Normally, this would raise an error message, but here it checks to see if the method is defined in Twitter::Client first. If the method exists there, the call is sent to a new instance of Client, otherwise the original method_missing defined in Ruby's Kernal module is called and an error is raised.
lib/twitter.rb and Rails Console
# twitter.rb

module Twitter
  extend Configuration

  def self.client(options={})
    Twitter::Client.new(options)
  end

  def self.method_missing(method, *args, &block)
    return super unless client.respond_to?(method)
    client.send(method, *args, &block)
  end
end
________________________________________

# Rails Console

> Twitter.user('arailsdemo')
  => <#Hashie::Mash contributors_enabled=false ...>
> Twitter.foo
  => NoMethodError: undefined method `foo' for Twitter:Module

The Twitter Client

In client.rb, the Client class inherits from 'API' and then include a bunch of modules, one of them being Twitter::Client::Tweets.

The API class is simple. It allows us to change some of the configuration settings on initialization. Also, it includes the Connection, Request, and Authentication modules.
lib/twitter/client.rb and lib/twitter/api.rb
# client.rb

module Twitter
  class Client < API
    ...
    include Twitter::Client::Tweets
    ...
    include Twitter::Client::User
    ...
________________________________________

# api.rb

module Twitter
  class API
    attr_accessor *Configuration::VALID_OPTIONS_KEYS

    def initialize(options={})
      options = Twitter.options.merge(options)
      Configuration::VALID_OPTIONS_KEYS.each do |key|
        send("#{key}=", options[key])
      end
    end

    include Connection
    include Request
    include Authentication

Sending a Twitter Update

Creating a Twitter update is simple with this gem. All we need to do is to create a new client and call #update. That's great news because it saves us from a lot of code otherwise.

Before we get to that method, we should note that our basic interaction with the Twitter website is through their REST API. (Searching through Twitter is done through a separate Search API.) So if we want to create a tweet, we need to send a POST request to this Twitter URL: http://api.twitter.com/version/statuses/update.format. If we look back into the Configuration module, we see the first part of this URL defined as a constant.

The update method is in tweets.rb. As expected, it calls #post with the rest of the Twitter URL that we want to call. #update also takes the tweet message and stores it in the options hash. #post is in request.rb and is basically the #request method in different form. #request then calls #connection.
REAMDE.mkd, lib/twitter/configuration.rb, lib/twitter/client/tweets.rb, lib/twitter/request.rb
# README.mkd

client = Twitter::Client.new
client.update("I just posted a status update via the Twitter Ruby Gem!")
________________________________________

# configuration.rb

module Twitter
  module Configuration
    ...
    DEFAULT_ENDPOINT        = 'https://api.twitter.com/1/'.freeze
________________________________________

# tweets.rb

module Twitter
  class Client
    module Tweets
      ...
      def update(status, options={})
        response = post('statuses/update', options.merge(:status => status))
        format.to_s.downcase == 'xml' ? response['status'] : response
      end
________________________________________

# request.rb

module Twitter
  module Request
    ...
    def post(path, options={}, raw=false)
      request(:post, path, options, raw)
    end

    private

    def request(method, path, options, raw)
      response = connection(raw).send(method) do |request|
        ...

Establishing The Connection To Twitter

In the Connection module, #connection builds some options for our POST request. The actual POST request, though, is going to be delegated to the Faraday gem.

Faraday is a 'modular HTTP client library using middleware heavily inspired by Rack.' The Faraday::Connection object will be built to use various middleware. The Parse and Mashify classes are defined in the faraday_middleware gem. Parse will parse json and xml responses from Twitter. Mashify will take the response hash and convert it to a Hashie::Mash object using the hashie gem. This will allow "you to create pseudo-objects that have method-like accessors for hash keys."

The other classes are defined in the Twitter gem. Multipart will handle files (such as images) from Twitter. The RaiseHttp4xx and RaiseHttp5xx classes will handle the HTTP status codes for client and server errors respectively. For some of our methods like Twitter::Client#update, we'll have to supply the OAuth credentials that we defined in the configuration. This is done with the OAuth class with the help of the simple_oath gem to build the OAuth headers.
lib/twitter/connection.rb, lib/twitter/authentication.rb and twitter/lib/faraday/oauth.rb
# connection.rb

require 'faraday_middleware'

module Twitter
  module Connection
    private

    def connection(raw=false)
      options = {
        :headers => {'Accept' => "application/#{format}", 'User-Agent' => user_agent},
        :proxy => proxy,
        :ssl => {:verify => false},
        :url => api_endpoint,
      }

      Faraday::Connection.new(options) do |connection|
        connection.use Faraday::Request::Multipart
        connection.use Faraday::Request::OAuth, authentication if authenticated?
        connection.adapter(adapter)
        connection.use Faraday::Response::RaiseHttp5xx
        connection.use Faraday::Response::Parse unless raw
        connection.use Faraday::Response::RaiseHttp4xx
        connection.use Faraday::Response::Mashify unless raw
      end
    end
  end
________________________________________

# autentication.rb

module Twitter
  module Authentication
    private

    def authentication
      {
        :consumer_key => consumer_key,
        :consumer_secret => consumer_secret,
        :token => oauth_token,
        :token_secret => oauth_token_secret
      }
    end

    def authenticated?
      authentication.values.all?
    end
  end
end
________________________________________

# oauth.rb

require 'faraday'
require 'simple_oauth'

module Faraday
  class Request::OAuth < Faraday::Middleware
    def call(env)
      params = env[:body].is_a?(Hash) ? env[:body] : {}
      signature_params = params.reject{|k,v| v.respond_to?(:content_type) }
      header = SimpleOAuth::Header.new(env[:method], env[:url], signature_params, @options)

      env[:request_headers]['Authorization'] = header.to_s

      @app.call(env)
    end

    def initialize(app, options)
      @app, @options = app, options
    end
  end
end

Back To #request

The call to #connection will return a Faraday::Connection instance. In #request, we will then send a message to this instance telling it to run its #post method with the given block. (This is an example of the metaprogramming technique called 'dynamic dispatch.'

#post is basically the #run_request method, and in this method is where the block that we provided earlier will be yielded to. In our case, the block will set the request path and body (which contains our tweet.) The actual HTTP request is made through the Faraday::Request class (not shown.)
lib/twitter/request.rb, faraday/lib/faraday/connection.rb
# request.rb

module Twitter
  module Request
    ...
    private

    def request(method, path, options, raw)
      response = connection(raw).send(method) do |request|
        case method
        when :get, :delete
          request.url(formatted_path(path), options)
        when :post, :put
          request.path = formatted_path(path)
          request.body = options
        end
      end
      raw ? response : response.body
    end

    def formatted_path(path)
      [path, format].compact.join('.')
    end
________________________________________

# connection.rb

module Faraday
  class Connection
    ...
    def post(url = nil, body = nil, headers = nil, &block)
      run_request(:post, url, body, headers, &block)
    end
    ...
    def run_request(method, url, body, headers)
      if !METHODS.include?(method)
        raise ArgumentError, "unknown http method: #{method}"
      end

      Request.run(self, method) do |req|
        req.url(url) if url
        req.headers.update(headers) if headers
        req.body = body if body
        yield req if block_given?
      end
    end

The End Result

With all of that code that the Twitter gem developers have thankfully allowed us to use, we can easily make a tweet to our Twitter account.
Rails Console
> client = Twitter.client
> response = client.update('test')
  => <#Hashie::Mash contributors=nil coordinates=nil ...>
> response.user.url
  => "http://www.arailsdemo.com"

> Twitter.oauth_token = 'bad_token'
> client = Twitter.client
> client.update('test')
  => Twitter::Unauthorized: POST https://api.twitter.com/1/statuses/update.json: 401 Unauthorized: Invalid / expired Token ...

    Comments

(Please login to submit comments.)



View All Posts