cli.rb 9.57 KB
module Twurl
  class CLI
    SUPPORTED_COMMANDS     = %w(authorize accounts alias set)
    DEFAULT_COMMAND        = 'request'
    PATH_PATTERN           = /^\/\w+/
    PROTOCOL_PATTERN       = /^\w+:\/\//
    README                 = File.dirname(__FILE__) + '/../../README'
    @output              ||= STDOUT
    class NoPathFound < Exception
    end

    class << self
      attr_accessor :output

      def run(args)
        begin
          options = parse_options(args)
        rescue NoPathFound => e
          exit
        end
        dispatch(options)
      end

      def dispatch(options)
        client     = OAuthClient.load_from_options(options)
        controller = case options.command
                     when 'authorize'
                       AuthorizationController
                     when 'accounts'
                       AccountInformationController
                     when 'alias'
                       AliasesController
                     when 'set'
                       ConfigurationController
                     when 'request'
                       RequestController
                     end
        controller.dispatch(client, options)
      rescue Twurl::Exception => exception
        abort(exception.message)
      end

      def parse_options(args)
        arguments = args.dup

        Twurl.options         = Options.new
        Twurl.options.trace   = false
        Twurl.options.data    = {}
        Twurl.options.headers = {}
        Twurl.options.upload  = {}
        Twurl.options.upload['file'] = []

        option_parser = OptionParser.new do |o|
          o.extend AvailableOptions

          o.banner = <<-BANNER
Usage: twurl authorize --consumer-key key --consumer-secret secret
       twurl [options] /1.1/statuses/home_timeline.json

Supported Commands: #{SUPPORTED_COMMANDS.sort.join(', ')}
          BANNER

          o.section "Getting started:" do
            tutorial
          end

          o.section "Authorization options:" do
            username
            password
            consumer_key
            consumer_secret
            access_token
            token_secret
          end

          o.section "Common options:" do
            trace
            data
            headers
            host
            quiet
            disable_ssl
            request_method
            help
            version
            proxy
            file
            filefield
            base64
            read_timeout
          end
        end

        arguments                 = option_parser.parse!(args)
        Twurl.options.command     = extract_command!(arguments)
        Twurl.options.path        = extract_path!(arguments)

        if Twurl.options.command == DEFAULT_COMMAND and Twurl.options.path.nil?
          CLI.puts option_parser
          raise NoPathFound, "No path found"
        end

        Twurl.options.subcommands = arguments
        Twurl.options
      end

      def output
        if Twurl.options && Twurl.options.output
          Twurl.options.output
        else
          @output
        end
      end

      def print(*args, &block)
        output.print(*args, &block)
        output.flush if output.respond_to?(:flush)
      end

      def puts(*args, &block)
        output.puts(*args, &block)
        output.flush if output.respond_to?(:flush)
      end

      def prompt_for(label)
        system "stty -echo"
        CLI.print "#{label}: "
        result = STDIN.gets.chomp
        CLI.puts
        result
      rescue Interrupt
        exit
      ensure
        system "stty echo"
      end

      private
        def extract_command!(arguments)
          if SUPPORTED_COMMANDS.include?(arguments.first)
            arguments.shift
          else
            DEFAULT_COMMAND
          end
        end

        def extract_path!(arguments)
          path = nil
          arguments.each_with_index do |argument, index|
            if argument[PATH_PATTERN]
              path_with_params = arguments.slice!(index)
              path, params = path_with_params.split("?", 2)
              if params
                path += "?" + escape_params(params)
              end
              break
            end
          end
          path
        end

        def escape_params(params)
          split_params = params.split("&").map do |param|
            key, value = param.split('=', 2)
            CGI::escape(key) + '=' + CGI::escape(value)
          end
          split_params.join("&")
        end
    end

    module AvailableOptions
      def options
        Twurl.options
      end

      def section(heading, &block)
        separator ""
        separator heading

        instance_eval(&block)
      end

      def tutorial
        on('-T', '--tutorial', "Narrative overview of how to get started using Twurl") do
          CLI.puts IO.read(README)
          exit
        end
      end

      def consumer_key
        on('-c', '--consumer-key [key]', "Your consumer key (required)") do |key|
          options.consumer_key = key ? key : CLI.prompt_for('Consumer key')
        end
      end

      def consumer_secret
        on('-s', '--consumer-secret [secret]', "Your consumer secret (required)") do |secret|
          options.consumer_secret = secret ? secret : CLI.prompt_for('Consumer secret')
        end
      end

      def access_token
        on('-a', '--access-token [token]', 'Your access token') do |token|
          options.access_token = token
        end
      end

      def token_secret
        on('-S', '--token-secret [secret]', "Your token secret") do |secret|
          options.token_secret = secret
        end
      end

      def username
        on('-u', '--username [username]', 'Username of account to authorize (required)') do |username|
          options.username = username
        end
      end

      def password
        on('-p', '--password [password]', 'Password of account to authorize (required)') do |password|
          options.password = password ? password : CLI.prompt_for('Password')
        end
      end

      def trace
        on('-t', '--[no-]trace', 'Trace request/response traffic (default: --no-trace)') do |trace|
          options.trace = trace
        end
      end

      def data
        on('-d', '--data [data]', 'Sends the specified data in a POST request to the HTTP server.') do |data|
          data.split('&').each do |pair|
            key, value = pair.split('=', 2)
            options.data[key] = value
          end
        end
      end

      def headers
        on('-A', '--header [header]', 'Adds the specified header to the request to the HTTP server.') do |header|
          key, value = header.split(': ')
          options.headers[key] = value
        end
      end

      def host
        on('-H', '--host [host]', 'Specify host to make requests to (default: api.twitter.com)') do |host|
          if host[PROTOCOL_PATTERN]
            protocol, protocolless_host = host.split(PROTOCOL_PATTERN, 2)
            options.host = protocolless_host
          else
            options.host = host
          end
        end
      end

      def quiet
        on('-q', '--quiet', 'Suppress all output (default: output is printed to STDOUT)') do |quiet|
          options.output = StringIO.new
        end
      end

      def disable_ssl
        on('-U', '--no-ssl', 'Disable SSL (default: SSL is enabled)') do |use_ssl|
          options.protocol = 'http'
        end
      end

      def request_method
        on('-X', '--request-method [method]', 'Request method (default: GET)') do |request_method|
          options.request_method = request_method.downcase
        end
      end

      def help
        on_tail("-h", "--help", "Show this message") do
          CLI.puts self
          exit
        end
      end

      def version
        on_tail("-v", "--version", "Show version") do
          CLI.puts Version
          exit
        end
      end

      def proxy
        on('-P', '--proxy [proxy]', 'Specify HTTP proxy to forward requests to (default: No proxy)') do |proxy|
          options.proxy = proxy
        end
      end

      def file
        on('-f', '--file [path_to_file]', 'Specify the path to the file to upload') do |file|
          if File.file?(file)
            options.upload['file'] << file
          else 
            CLI.puts "ERROR: File not found"
            exit
          end
        end
      end
      
      def filefield 
        on('-F', '--file-field [field_name]', 'Specify the POST parameter name for the file upload data (default: media)') do |filefield|
          options.upload['filefield'] = filefield
        end
      end
      
      def base64 
        on('-b', '--base64', 'Encode the uploaded file as base64 (default: false)') do |base64|
          options.upload['base64'] = base64
        end
      end

      def read_timeout
        on('-y', '--read-timeout [read_timeout]', 'Http Read Timeout (default: Net::HTTP read_timeout)') do |read_timeout|
          options.read_timeout = read_timeout.to_i
        end
      end
    end
  end

    
  class Options < OpenStruct
    DEFAULT_REQUEST_METHOD = 'get'
    DEFAULT_HOST           = 'api.twitter.com'
    DEFAULT_PROTOCOL       = 'https'

    def oauth_client_options
      OAuthClient::OAUTH_CLIENT_OPTIONS.inject({}) do |options, option|
        options[option] = send(option)
        options
      end
    end

    def base_url
      "#{protocol}://#{host}"
    end

    def ssl?
      protocol == 'https'
    end

    def debug_output_io
      super || STDERR
    end

    def request_method
      super || (data.empty? ? DEFAULT_REQUEST_METHOD : 'post')
    end

    def protocol
      super || DEFAULT_PROTOCOL
    end

    def host
      super || DEFAULT_HOST
    end

    def proxy
      super || nil
    end
  end
end