public
Description: Makes http fun! Also, makes consuming restful web services dead easy.
Homepage:
Clone URL: git://github.com/jnunemaker/httparty.git
Click here to lend your support to: httparty and make a donation at www.pledgie.com !
httparty / lib / httparty.rb
100644 169 lines (145 sloc) 6.157 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
require 'net/http'
require 'net/https'
require 'uri'
require 'ostruct'
require 'rubygems'
require 'active_support'
 
directory = File.dirname(__FILE__)
$:.unshift(directory) unless $:.include?(directory) || $:.include?(File.expand_path(directory))
 
module HTTParty
  class UnsupportedFormat < StandardError; end
  class RedirectionTooDeep < StandardError; end
  
  def self.included(base)
    base.extend ClassMethods
  end
  
  AllowedFormats = {:xml => 'text/xml', :json => 'application/json'}
  
  module ClassMethods
    #
    # Set an http proxy
    #
    # class Twitter
    # include HTTParty
    # http_proxy http://myProxy, 1080
    # ....
    def http_proxy(addr=nil, port = nil)
@http_proxyaddr = addr
@http_proxyport = port
    end
 
    def base_uri(base_uri=nil)
      return @base_uri unless base_uri
      @base_uri = normalize_base_uri(base_uri)
    end
    
    # Warning: This is not thread safe most likely and
    # only works if you use one set of credentials. I
    # leave it because it is convenient on some occasions.
    def basic_auth(u, p)
      @auth = {:username => u, :password => p}
    end
    
    # Updates the default query string parameters
    # that should be appended to each request.
    def default_params(h={})
      raise ArgumentError, 'Default params must be a hash' unless h.is_a?(Hash)
      @default_params ||= {}
      return @default_params if h.blank?
      @default_params.merge!(h)
    end
 
    def headers(h={})
      raise ArgumentError, 'Headers must be a hash' unless h.is_a?(Hash)
      @headers ||= {}
      return @headers if h.blank?
      @headers.merge!(h)
    end
    
    def format(f)
      raise UnsupportedFormat, "Must be one of: #{AllowedFormats.keys.join(', ')}" unless AllowedFormats.key?(f)
      @format = f
    end
    
    # TODO: spec out this
    def get(path, options={})
      send_request 'get', path, options
    end
 
    # TODO: spec out this
    def post(path, options={})
      send_request 'post', path, options
    end
 
    # TODO: spec out this
    def put(path, options={})
      send_request 'put', path, options
    end
 
    # TODO: spec out this
    def delete(path, options={})
      send_request 'delete', path, options
    end
    
    private
      def http(uri) #:nodoc:
        http = Net::HTTP.new(uri.host, uri.port, @http_proxyaddr, @http_proxyport)
        http.use_ssl = (uri.port == 443)
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
        http
      end
      
      # FIXME: this method is doing way to much and needs to be split up
      # options can be any or all of:
      # query => hash of keys/values or a query string (foo=bar&baz=poo)
      # body => hash of keys/values or a query string (foo=bar&baz=poo)
      # headers => hash of headers to send request with
      # basic_auth => :username and :password to use as basic http authentication (overrides @auth class instance variable)
      # Raises exception Net::XXX (http error code) if an http error occured
      def send_request(method, path, options={}) #:nodoc:
        options = {:limit => 5}.merge(options)
        options[:limit] = 0 if options.delete(:no_follow)
        
        raise HTTParty::RedirectionTooDeep, 'HTTP redirects too deep' if options[:limit].to_i <= 0
        raise ArgumentError, 'only get, post, put and delete methods are supported' unless %w[get post put delete].include?(method.to_s)
        raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
        raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
        
        path = URI.parse(path)
        uri = path.relative? ? URI.parse("#{base_uri}#{path}") : path
        existing_query = uri.query ? "#{uri.query}&" : ''
        uri.query = if options[:query].blank?
          existing_query + default_params.to_query
        else
          existing_query + (options[:query].is_a?(Hash) ? default_params.merge(options[:query]).to_query : options[:query])
        end
        
        klass = Net::HTTP.const_get method.to_s.downcase.capitalize
        request = klass.new(uri.request_uri)
        request.body = options[:body].is_a?(Hash) ? options[:body].to_query : options[:body] unless options[:body].blank?
        basic_auth = options.delete(:basic_auth) || @auth
        request.initialize_http_header headers.merge(options[:headers] || {})
        request.basic_auth(basic_auth[:username], basic_auth[:password]) if basic_auth
        response = http(uri).request(request)
        @format ||= format_from_mimetype(response['content-type'])
        
        case response
        when Net::HTTPSuccess
          parse_response(response.body)
        when Net::HTTPRedirection
          options[:limit] -= 1
          send_request(method, response['location'], options)
        else
          response.instance_eval { class << self; attr_accessor :body_parsed; end }
          begin; response.body_parsed = parse_response(response.body); rescue; end
          response.error! # raises exception corresponding to http error Net::XXX
        end
 
      end
      
      def parse_response(body) #:nodoc:
        return nil if body.nil? or body.empty?
        case @format
        when :xml
          Hash.from_xml(body)
        when :json
          ActiveSupport::JSON.decode(body)
        else
          body
        end
      end
    
      # Makes it so uri is sure to parse stuff like google.com with the http
      def normalize_base_uri(url) #:nodoc:
        use_ssl = (url =~ /^https/) || url.include?(':443')
        url.chop! if url.ends_with?('/')
        url.gsub!(/^https?:\/\//i, '')
        "http#{'s' if use_ssl}://#{url}"
      end
      
      # Uses the HTTP Content-Type header to determine the format of the response
      # It compares the MIME type returned to the types stored in the AllowedFormats hash
      def format_from_mimetype(mimetype) #:nodoc:
        AllowedFormats.each { |k, v| return k if mimetype.include?(v) }
      end
  end
end