auser-rest-client 1.0

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,43 @@
1
+ module RestClient
2
+ module Mixin
3
+ module Response
4
+ attr_reader :net_http_res
5
+
6
+ # HTTP status code, always 200 since RestClient throws exceptions for
7
+ # other codes.
8
+ def code
9
+ @code ||= @net_http_res.code.to_i
10
+ end
11
+
12
+ # A hash of the headers, beautified with symbols and underscores.
13
+ # e.g. "Content-type" will become :content_type.
14
+ def headers
15
+ @headers ||= self.class.beautify_headers(@net_http_res.to_hash)
16
+ end
17
+
18
+ # Hash of cookies extracted from response headers
19
+ def cookies
20
+ @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
21
+ key, val = raw_c.split('=')
22
+ unless %w(expires domain path secure).member?(key)
23
+ out[key] = val
24
+ end
25
+ out
26
+ end
27
+ end
28
+
29
+ def self.included(receiver)
30
+ receiver.extend(RestClient::Mixin::Response::ClassMethods)
31
+ end
32
+
33
+ module ClassMethods
34
+ def beautify_headers(headers)
35
+ headers.inject({}) do |out, (key, value)|
36
+ out[key.gsub(/-/, '_').to_sym] = value.first
37
+ out
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,236 @@
1
+ require 'tempfile'
2
+
3
+ module RestClient
4
+ # This class is used internally by RestClient to send the request, but you can also
5
+ # access it internally if you'd like to use a method not directly supported by the
6
+ # main API. For example:
7
+ #
8
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
+ #
10
+ class Request
11
+ attr_reader :method, :url, :payload, :headers,
12
+ :cookies, :user, :password, :timeout, :open_timeout,
13
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
14
+ :raw_response
15
+
16
+ def self.execute(args)
17
+ new(args).execute
18
+ end
19
+
20
+ def initialize(args)
21
+ @method = args[:method] or raise ArgumentError, "must pass :method"
22
+ @url = args[:url] or raise ArgumentError, "must pass :url"
23
+ @headers = args[:headers] || {}
24
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
25
+ @payload = process_payload(args[:payload])
26
+ @user = args[:user]
27
+ @password = args[:password]
28
+ @timeout = args[:timeout]
29
+ @open_timeout = args[:open_timeout]
30
+ @raw_response = args[:raw_response] || false
31
+ @verify_ssl = args[:verify_ssl] || false
32
+ @ssl_client_cert = args[:ssl_client_cert] || nil
33
+ @ssl_client_key = args[:ssl_client_key] || nil
34
+ @ssl_ca_file = args[:ssl_ca_file] || nil
35
+ @tf = nil # If you are a raw request, this is your tempfile
36
+ end
37
+
38
+ def execute
39
+ execute_inner
40
+ rescue Redirect => e
41
+ @url = e.url
42
+ execute
43
+ end
44
+
45
+ def execute_inner
46
+ uri = parse_url_with_auth(url)
47
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
48
+ end
49
+
50
+ def make_headers(user_headers)
51
+ unless @cookies.empty?
52
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
53
+ end
54
+
55
+ default_headers.merge(user_headers).inject({}) do |final, (key, value)|
56
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
57
+ final
58
+ end
59
+ end
60
+
61
+ def net_http_class
62
+ if RestClient.proxy
63
+ proxy_uri = URI.parse(RestClient.proxy)
64
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
65
+ else
66
+ Net::HTTP
67
+ end
68
+ end
69
+
70
+ def net_http_request_class(method)
71
+ Net::HTTP.const_get(method.to_s.capitalize)
72
+ end
73
+
74
+ def parse_url(url)
75
+ url = "http://#{url}" unless url.match(/^http/)
76
+ URI.parse(url)
77
+ end
78
+
79
+ def parse_url_with_auth(url)
80
+ uri = parse_url(url)
81
+ @user = uri.user if uri.user
82
+ @password = uri.password if uri.password
83
+ uri
84
+ end
85
+
86
+ def process_payload(p=nil, parent_key=nil)
87
+ unless p.is_a?(Hash)
88
+ p
89
+ else
90
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
91
+ p.keys.map do |k|
92
+ key = parent_key ? "#{parent_key}[#{k}]" : k
93
+ if p[k].is_a? Hash
94
+ process_payload(p[k], key)
95
+ else
96
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
97
+ "#{key}=#{value}"
98
+ end
99
+ end.join("&")
100
+ end
101
+ end
102
+
103
+ def transmit(uri, req, payload)
104
+ setup_credentials(req)
105
+
106
+ net = net_http_class.new(uri.host, uri.port)
107
+ net.use_ssl = uri.is_a?(URI::HTTPS)
108
+ if @verify_ssl == false
109
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
110
+ elsif @verify_ssl.is_a? Integer
111
+ net.verify_mode = @verify_ssl
112
+ end
113
+ net.cert = @ssl_client_cert if @ssl_client_cert
114
+ net.key = @ssl_client_key if @ssl_client_key
115
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
116
+ net.read_timeout = @timeout if @timeout
117
+ net.open_timeout = @open_timeout if @open_timeout
118
+
119
+ display_log request_log
120
+
121
+ net.start do |http|
122
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
123
+ result = process_result(res)
124
+ display_log response_log(res)
125
+
126
+ if result.kind_of?(String) or @method == :head
127
+ Response.new(result, res)
128
+ elsif @raw_response
129
+ RawResponse.new(@tf, res)
130
+ else
131
+ nil
132
+ end
133
+ end
134
+ rescue EOFError
135
+ raise RestClient::ServerBrokeConnection
136
+ rescue Timeout::Error
137
+ raise RestClient::RequestTimeout
138
+ end
139
+
140
+ def setup_credentials(req)
141
+ req.basic_auth(user, password) if user
142
+ end
143
+
144
+ def fetch_body(http_response)
145
+ if @raw_response
146
+ # Taken from Chef, which as in turn...
147
+ # Stolen from http://www.ruby-forum.com/topic/166423
148
+ # Kudos to _why!
149
+ @tf = Tempfile.new("rest-client")
150
+ size, total = 0, http_response.header['Content-Length'].to_i
151
+ http_response.read_body do |chunk|
152
+ @tf.write(chunk)
153
+ size += chunk.size
154
+ if size == 0
155
+ display_log("#{@method} #{@url} done (0 length file)")
156
+ elsif total == 0
157
+ display_log("#{@method} #{@url} (zero content length)")
158
+ else
159
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
160
+ end
161
+ end
162
+ @tf.close
163
+ @tf
164
+ else
165
+ http_response.read_body
166
+ end
167
+ http_response
168
+ end
169
+
170
+ def process_result(res)
171
+ if res.code =~ /\A2\d{2}\z/
172
+ # We don't decode raw requests
173
+ unless @raw_response
174
+ decode res['content-encoding'], res.body if res.body
175
+ end
176
+ elsif %w(301 302 303).include? res.code
177
+ url = res.header['Location']
178
+
179
+ if url !~ /^http/
180
+ uri = URI.parse(@url)
181
+ uri.path = "/#{url}".squeeze('/')
182
+ url = uri.to_s
183
+ end
184
+
185
+ raise Redirect, url
186
+ elsif res.code == "304"
187
+ raise NotModified, res
188
+ elsif res.code == "401"
189
+ raise Unauthorized, res
190
+ elsif res.code == "404"
191
+ raise ResourceNotFound, res
192
+ else
193
+ raise RequestFailed, res
194
+ end
195
+ end
196
+
197
+ def decode(content_encoding, body)
198
+ if content_encoding == 'gzip' and not body.empty?
199
+ Zlib::GzipReader.new(StringIO.new(body)).read
200
+ elsif content_encoding == 'deflate'
201
+ Zlib::Inflate.new.inflate(body)
202
+ else
203
+ body
204
+ end
205
+ end
206
+
207
+ def request_log
208
+ out = []
209
+ out << "RestClient.#{method} #{url.inspect}"
210
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
211
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
212
+ out.join(', ')
213
+ end
214
+
215
+ def response_log(res)
216
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
217
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
218
+ end
219
+
220
+ def display_log(msg)
221
+ return unless log_to = RestClient.log
222
+
223
+ if log_to == 'stdout'
224
+ STDOUT.puts msg
225
+ elsif log_to == 'stderr'
226
+ STDERR.puts msg
227
+ else
228
+ File.open(log_to, 'a') { |f| f.puts msg }
229
+ end
230
+ end
231
+
232
+ def default_headers
233
+ { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' }
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,146 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil)
40
+ @url = url
41
+ if options.class == Hash
42
+ @options = options
43
+ else # compatibility with previous versions
44
+ @options = { :user => options, :password => backwards_compatibility }
45
+ end
46
+ end
47
+
48
+ def get(additional_headers={})
49
+ Request.execute(options.merge(
50
+ :method => :get,
51
+ :url => url,
52
+ :headers => headers.merge(additional_headers)
53
+ ))
54
+ end
55
+
56
+ def post(payload, additional_headers={})
57
+ Request.execute(options.merge(
58
+ :method => :post,
59
+ :url => url,
60
+ :payload => payload,
61
+ :headers => headers.merge(additional_headers)
62
+ ))
63
+ end
64
+
65
+ def put(payload, additional_headers={})
66
+ Request.execute(options.merge(
67
+ :method => :put,
68
+ :url => url,
69
+ :payload => payload,
70
+ :headers => headers.merge(additional_headers)
71
+ ))
72
+ end
73
+
74
+ def delete(additional_headers={})
75
+ Request.execute(options.merge(
76
+ :method => :delete,
77
+ :url => url,
78
+ :headers => headers.merge(additional_headers)
79
+ ))
80
+ end
81
+
82
+ def to_s
83
+ url
84
+ end
85
+
86
+ def user
87
+ options[:user]
88
+ end
89
+
90
+ def password
91
+ options[:password]
92
+ end
93
+
94
+ def headers
95
+ options[:headers] || {}
96
+ end
97
+
98
+ def timeout
99
+ options[:timeout]
100
+ end
101
+
102
+ def open_timeout
103
+ options[:open_timeout]
104
+ end
105
+
106
+ # Construct a subresource, preserving authentication.
107
+ #
108
+ # Example:
109
+ #
110
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
111
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
112
+ #
113
+ # This is especially useful if you wish to define your site in one place and
114
+ # call it in multiple locations:
115
+ #
116
+ # def orders
117
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
118
+ # end
119
+ #
120
+ # orders.get # GET http://example.com/orders
121
+ # orders['1'].get # GET http://example.com/orders/1
122
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
123
+ #
124
+ # Nest resources as far as you want:
125
+ #
126
+ # site = RestClient::Resource.new('http://example.com')
127
+ # posts = site['posts']
128
+ # first_post = posts['1']
129
+ # comments = first_post['comments']
130
+ # comments.post 'Hello', :content_type => 'text/plain'
131
+ #
132
+ def [](suburl)
133
+ self.class.new(concat_urls(url, suburl), options)
134
+ end
135
+
136
+ def concat_urls(url, suburl) # :nodoc:
137
+ url = url.to_s
138
+ suburl = suburl.to_s
139
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
140
+ url + suburl
141
+ else
142
+ "#{url}/#{suburl}"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module RestClient
4
+ # The response from RestClient looks like a string, but is actually one of
5
+ # these. 99% of the time you're making a rest call all you care about is
6
+ # the body, but on the occassion you want to fetch the headers you can:
7
+ #
8
+ # RestClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ class Response < String
11
+
12
+ include RestClient::Mixin::Response
13
+
14
+ def initialize(string, net_http_res)
15
+ @net_http_res = net_http_res
16
+ super(string || "")
17
+ end
18
+
19
+ end
20
+ end