rest-client 1.3.1 → 1.4.0.a

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rest-client might be problematic. Click here for more details.

@@ -49,11 +49,12 @@ See RestClient::Resource module docs for details.
49
49
 
50
50
  See RestClient::Resource docs for details.
51
51
 
52
- == Exceptions
52
+ == Exceptions (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
53
53
 
54
54
  * for results code between 200 and 206 a RestClient::Response will be returned
55
- * for results code between 301 and 303 the redirection will be automatically followed
56
- * for other result codes a RestClient::Exception holding the Response will be raised, a specific exception class will be thrown for know error codes
55
+ * for results code 301 and 302 the redirection will be followed if the request is a get or a head
56
+ * for result code 303 the redirection will be followed and the request transformed into a get
57
+ * for other cases a RestClient::Exception holding the Response will be raised, a specific exception class will be thrown for know error codes
57
58
 
58
59
  RestClient.get 'http://example.com/resource'
59
60
  ➔ RestClient::ResourceNotFound: RestClient::ResourceNotFound
@@ -63,19 +64,19 @@ See RestClient::Resource docs for details.
63
64
  rescue => e
64
65
  e.response
65
66
  end
66
- "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>..."
67
+ 404 Resource Not Found | text/html 282 bytes
67
68
 
68
69
  == Result handling
69
70
 
70
71
  A block can be passed to the RestClient method, this block will then be called with the Response.
71
- Response.return! can be called to invoke the default response's behavior (return the Response for 200..206, raise an exception in other cases).
72
+ Response.return! can be called to invoke the default response's behavior.
72
73
 
73
74
  # Don't raise exceptions but return the response
74
- RestClient.get('http://example.com/resource'){|response| response}
75
- "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>..."
75
+ RestClient.get('http://example.com/resource'){|response| response }
76
+ 404 Resource Not Found | text/html 282 bytes
76
77
 
77
78
  # Manage a specific error code
78
- RestClient.get('http://my-rest-service.com/resource'){ |response|
79
+ RestClient.get('http://my-rest-service.com/resource'){ |response, &block|
79
80
  case response.code
80
81
  when 200
81
82
  p "It worked !"
@@ -83,7 +84,19 @@ Response.return! can be called to invoke the default response's behavior (return
83
84
  when 423
84
85
  raise SomeCustomExceptionIfYouWant
85
86
  else
86
- response.return!
87
+ response.return! &block
88
+ end
89
+ }
90
+
91
+ # Follow redirections for all request types and not only for get and head
92
+ # RFC : "If the 301 (or 302) status code is received in response to a request other than GET or HEAD,
93
+ # the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user,
94
+ # since this might change the conditions under which the request was issued."
95
+ RestClient.get('http://my-rest-service.com/resource'){ |response, &block|
96
+ if [301, 302].include? response.code
97
+ response.follow_redirection &block
98
+ else
99
+ response.return! &block
87
100
  end
88
101
  }
89
102
 
@@ -103,17 +116,17 @@ For cases not covered by the general API, you can use the RestClient::Resource c
103
116
  The restclient shell command gives an IRB session with RestClient already loaded:
104
117
 
105
118
  $ restclient
106
- RestClient.get 'http://example.com'
119
+ >> RestClient.get 'http://example.com'
107
120
 
108
121
  Specify a URL argument for get/post/put/delete on that resource:
109
122
 
110
123
  $ restclient http://example.com
111
- put '/resource', 'data'
124
+ >> put '/resource', 'data'
112
125
 
113
126
  Add a user and password for authenticated resources:
114
127
 
115
128
  $ restclient https://example.com user pass
116
- delete '/private/resource'
129
+ >> delete '/private/resource'
117
130
 
118
131
  Create ~/.restclient for named sessions:
119
132
 
@@ -197,6 +210,22 @@ extract and set headers for them as needed:
197
210
 
198
211
  Self-signed certificates can be generated with the openssl command-line tool.
199
212
 
213
+ == Hook
214
+
215
+ RestClient.add_before_execution_proc add a Proc to be called before each execution, it's handy if you need a direct access to the http request.
216
+
217
+ Example:
218
+
219
+ # Add oath support using the oauth gem
220
+ require 'oauth'
221
+ access_token = ...
222
+
223
+ RestClient.add_before_execution_proc do |req, params|
224
+ access_token.sign! req
225
+ end
226
+
227
+ RestClient.get 'http://example.com'
228
+
200
229
  == Meta
201
230
 
202
231
  Written by Adam Wiggins, major modifications by Blake Mizerany, maintained by Julien Kirch
data/Rakefile CHANGED
@@ -3,16 +3,19 @@ require 'rake'
3
3
  require 'jeweler'
4
4
 
5
5
  Jeweler::Tasks.new do |s|
6
- s.name = "rest-client"
7
- s.description = "A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
8
- s.summary = "Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions."
9
- s.author = "Adam Wiggins"
10
- s.email = "rest.client@librelist.com"
11
- s.homepage = "http://github.com/archiloque/rest-client"
12
- s.rubyforge_project = "rest-client"
13
- s.has_rdoc = true
14
- s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
15
- s.executables = %w(restclient)
6
+ s.name = "rest-client"
7
+ s.description = "A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
8
+ s.summary = "Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions."
9
+ s.author = "Adam Wiggins"
10
+ s.email = "rest.client@librelist.com"
11
+ s.homepage = "http://github.com/archiloque/rest-client"
12
+ s.rubyforge_project = "rest-client"
13
+ s.has_rdoc = true
14
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
15
+ s.executables = %w(restclient)
16
+ s.add_dependency("mime-types", ">= 1.16")
17
+ s.add_development_dependency("webmock", ">= 0.9.1")
18
+ s.add_development_dependency("rspec")
16
19
  end
17
20
 
18
21
  Jeweler::RubyforgeTasks.new
@@ -23,21 +26,21 @@ require 'spec/rake/spectask'
23
26
 
24
27
  desc "Run all specs"
25
28
  Spec::Rake::SpecTask.new('spec') do |t|
26
- t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
27
- t.spec_files = FileList['spec/*_spec.rb']
29
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
30
+ t.spec_files = FileList['spec/*_spec.rb']
28
31
  end
29
32
 
30
33
  desc "Print specdocs"
31
34
  Spec::Rake::SpecTask.new(:doc) do |t|
32
- t.spec_opts = ["--format", "specdoc", "--dry-run"]
33
- t.spec_files = FileList['spec/*_spec.rb']
35
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
36
+ t.spec_files = FileList['spec/*_spec.rb']
34
37
  end
35
38
 
36
39
  desc "Run all examples with RCov"
37
40
  Spec::Rake::SpecTask.new('rcov') do |t|
38
- t.spec_files = FileList['spec/*_spec.rb']
39
- t.rcov = true
40
- t.rcov_opts = ['--exclude', 'examples']
41
+ t.spec_files = FileList['spec/*_spec.rb']
42
+ t.rcov = true
43
+ t.rcov_opts = ['--exclude', 'examples']
41
44
  end
42
45
 
43
46
  task :default => :spec
@@ -47,11 +50,11 @@ task :default => :spec
47
50
  require 'rake/rdoctask'
48
51
 
49
52
  Rake::RDocTask.new do |t|
50
- t.rdoc_dir = 'rdoc'
51
- t.title = "rest-client, fetch RESTful resources effortlessly"
52
- t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
53
- t.options << '--charset' << 'utf-8'
54
- t.rdoc_files.include('README.rdoc')
55
- t.rdoc_files.include('lib/*.rb')
53
+ t.rdoc_dir = 'rdoc'
54
+ t.title = "rest-client, fetch RESTful resources effortlessly"
55
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
56
+ t.options << '--charset' << 'utf-8'
57
+ t.rdoc_files.include('README.rdoc')
58
+ t.rdoc_files.include('lib/*.rb')
56
59
  end
57
60
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.1
1
+ 1.4.0.a
data/history.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 1.4.0
2
+
3
+ - Response is no more a String, and the mixin is replaced by an abstract_response, existing calls are redirected to response body with a warning.
4
+ - enable repeated parameters RestClient.post 'http://example.com/resource', :param1 => ['one', 'two', 'three'], => :param2 => 'foo' (patch provided by Rodrigo Panachi)
5
+ - fixed the redirect code concerning relative path and query string combination (patch provided by Kevin Read)
6
+ - redirection code moved to Response so redirection can be customized using the block syntax
7
+ - only get and head redirections are now followed by default, as stated in the specification
8
+ - added RestClient.add_before_execution_proc to hack the http request, like for oauth
9
+
10
+ The response change may be breaking in rare cases.
11
+
1
12
  # 1.3.1
2
13
 
3
14
  - added compatibility to enable responses in exception to act like Net::HTTPResponse
@@ -11,6 +22,8 @@
11
22
  - all http error codes have now a corresponding exception class and all of them contain the Reponse -> this means that the raised exception can be different
12
23
  - changed "Content-Disposition: multipart/form-data" to "Content-Disposition: form-data" per RFC 2388 (patch provided by Kyle Crawford)
13
24
 
25
+ The only breaking change should be the exception classes, but as the new classes inherits from the existing ones, the breaking cases should be rare.
26
+
14
27
  # 1.2.0
15
28
 
16
29
  - formatting changed from tabs to spaces
@@ -11,7 +11,7 @@ end
11
11
 
12
12
  require File.dirname(__FILE__) + '/restclient/exceptions'
13
13
  require File.dirname(__FILE__) + '/restclient/request'
14
- require File.dirname(__FILE__) + '/restclient/mixin/response'
14
+ require File.dirname(__FILE__) + '/restclient/abstract_response'
15
15
  require File.dirname(__FILE__) + '/restclient/response'
16
16
  require File.dirname(__FILE__) + '/restclient/raw_response'
17
17
  require File.dirname(__FILE__) + '/restclient/resource'
@@ -126,6 +126,7 @@ module RestClient
126
126
  else
127
127
  file_logger = Class.new do
128
128
  attr_writer :target_file
129
+
129
130
  def << obj
130
131
  File.open(@target_file, 'a') { |f| f.puts obj }
131
132
  end
@@ -148,4 +149,16 @@ module RestClient
148
149
  @@env_log || @@log
149
150
  end
150
151
 
152
+ @@before_execution_procs = []
153
+
154
+ # Add a Proc to be called before each request in executed.
155
+ # The proc parameters will be the http request and the request params.
156
+ def self.add_before_execution_proc &proc
157
+ @@before_execution_procs << proc
158
+ end
159
+
160
+ def self.before_execution_procs # :nodoc:
161
+ @@before_execution_procs
162
+ end
163
+
151
164
  end
@@ -0,0 +1,87 @@
1
+ module RestClient
2
+
3
+ class AbstractResponse
4
+
5
+ attr_reader :net_http_res, :args
6
+
7
+ def initialize net_http_res, args
8
+ @net_http_res = net_http_res
9
+ @args = args
10
+ end
11
+
12
+ # HTTP status code
13
+ def code
14
+ @code ||= @net_http_res.code.to_i
15
+ end
16
+
17
+ # A hash of the headers, beautified with symbols and underscores.
18
+ # e.g. "Content-type" will become :content_type.
19
+ def headers
20
+ @headers ||= self.class.beautify_headers(@net_http_res.to_hash)
21
+ end
22
+
23
+ # The raw headers.
24
+ def raw_headers
25
+ @raw_headers ||= @net_http_res.to_hash
26
+ end
27
+
28
+ # Hash of cookies extracted from response headers
29
+ def cookies
30
+ @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content|
31
+ # correctly parse comma-separated cookies containing HTTP dates (which also contain a comma)
32
+ cookie_content.split(/,\s*/).inject([""]) { |array, blob|
33
+ blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob)
34
+ array
35
+ }.each do |cookie|
36
+ next if cookie.empty?
37
+ key, *val = cookie.split(";").first.split("=")
38
+ out[key] = val.join("=")
39
+ end
40
+ out
41
+ end
42
+ end
43
+
44
+ # Return the default behavior corresponding to the response code:
45
+ # the response itself for code in 200..206, redirection for 301 and 302 in get and head cases, redirection for 303 and an exception in other cases
46
+ def return! &block
47
+ if (200..206).include? code
48
+ self
49
+ elsif [301, 302].include? code
50
+ unless [:get, :head].include? args[:method]
51
+ raise Exceptions::EXCEPTIONS_MAP[code], self
52
+ else
53
+ follow_redirection &block
54
+ end
55
+ elsif code == 303
56
+ args[:method] = :get
57
+ args.delete :payload
58
+ follow_redirection &block
59
+ elsif Exceptions::EXCEPTIONS_MAP[code]
60
+ raise Exceptions::EXCEPTIONS_MAP[code], self
61
+ else
62
+ raise RequestFailed, self
63
+ end
64
+ end
65
+
66
+ def inspect
67
+ "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
68
+ end
69
+
70
+ # Follow a redirection
71
+ def follow_redirection &block
72
+ url = headers[:location]
73
+ if url !~ /^http/
74
+ url = URI.parse(args[:url]).merge(url).to_s
75
+ end
76
+ args[:url] = url
77
+ Request.execute args, &block
78
+ end
79
+
80
+ def AbstractResponse.beautify_headers(headers)
81
+ headers.inject({}) do |out, (key, value)|
82
+ out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first
83
+ out
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,5 +1,44 @@
1
1
  module RestClient
2
2
 
3
+ STATUSES = {100 => 'Continue',
4
+ 101 => 'Switching Protocols',
5
+ 200 => 'OK',
6
+ 201 => 'Created',
7
+ 202 => 'Accepted',
8
+ 203 => 'Non-Authoritative Information',
9
+ 204 => 'No Content',
10
+ 205 => 'Reset Content',
11
+ 206 => 'Partial Content',
12
+ 300 => 'Multiple Choices',
13
+ 301 => 'Moved Permanently',
14
+ 302 => 'Found',
15
+ 303 => 'See Other',
16
+ 304 => 'Not Modified',
17
+ 305 => 'Use Proxy',
18
+ 400 => 'Bad Request',
19
+ 401 => 'Unauthorized',
20
+ 403 => 'Forbidden',
21
+ 404 => 'Resource Not Found',
22
+ 405 => 'Method Not Allowed',
23
+ 406 => 'Not Acceptable',
24
+ 407 => 'Proxy Authentication Required',
25
+ 408 => 'Request Timeout',
26
+ 409 => 'Conflict',
27
+ 410 => 'Gone',
28
+ 411 => 'Length Required',
29
+ 412 => 'Precondition Failed',
30
+ 413 => 'Request Entity Too Large',
31
+ 414 => 'Request-URI Too Long',
32
+ 415 => 'Unsupported Media Type',
33
+ 416 => 'Requested Range Not Satisfiable',
34
+ 417 => 'Expectation Failed',
35
+ 500 => 'Internal Server Error',
36
+ 501 => 'Not Implemented',
37
+ 502 => 'Bad Gateway',
38
+ 503 => 'Service Unavailable',
39
+ 504 => 'Gateway Timeout',
40
+ 505 => 'HTTP Version Not Supported'}
41
+
3
42
  # Compatibility : make the Response act like a Net::HTTPResponse when needed
4
43
  module ResponseForException
5
44
  def method_missing symbol, *args
@@ -34,7 +73,7 @@ module RestClient
34
73
  end
35
74
 
36
75
  def http_body
37
- @response
76
+ @response.body
38
77
  end
39
78
 
40
79
  def inspect
@@ -65,35 +104,7 @@ module RestClient
65
104
  EXCEPTIONS_MAP = {}
66
105
  end
67
106
 
68
- {300 => 'Multiple Choices',
69
- 301 => 'Moved Permanently',
70
- 302 => 'Found',
71
- 303 => 'See Other',
72
- 304 => 'Not Modified',
73
- 305 => 'Use Proxy',
74
- 400 => 'Bad Request',
75
- 401 => 'Unauthorized',
76
- 403 => 'Forbidden',
77
- 404 => 'Resource Not Found',
78
- 405 => 'Method Not Allowed',
79
- 406 => 'Not Acceptable',
80
- 407 => 'Proxy Authentication Required',
81
- 408 => 'Request Timeout',
82
- 409 => 'Conflict',
83
- 410 => 'Gone',
84
- 411 => 'Length Required',
85
- 412 => 'Precondition Failed',
86
- 413 => 'Request Entity Too Large',
87
- 414 => 'Request-URI Too Long',
88
- 415 => 'Unsupported Media Type',
89
- 416 => 'Requested Range Not Satisfiable',
90
- 417 => 'Expectation Failed',
91
- 500 => 'Internal Server Error',
92
- 501 => 'Not Implemented',
93
- 502 => 'Bad Gateway',
94
- 503 => 'Service Unavailable',
95
- 504 => 'Gateway Timeout',
96
- 505 => 'HTTP Version Not Supported'}.each_pair do |code, message|
107
+ STATUSES.each_pair do |code, message|
97
108
 
98
109
  # Compatibility
99
110
  superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed
@@ -54,14 +54,17 @@ module RestClient
54
54
  # Flatten parameters by converting hashes of hashes to flat hashes
55
55
  # {keys1 => {keys2 => value}} will be transformed into {keys1[key2] => value}
56
56
  def flatten_params(params, parent_key = nil)
57
- result = {}
58
- params.keys.map do |key|
57
+ result = []
58
+ params.each do |key, value|
59
59
  calculated_key = parent_key ? "#{parent_key}[#{escape key}]" : escape(key)
60
- value = params[key]
61
60
  if value.is_a? Hash
62
- result.merge!(flatten_params(value, calculated_key))
61
+ result << flatten_params(value, calculated_key).flatten
62
+ elsif value.is_a? Array
63
+ value.each do |elem|
64
+ result << [calculated_key, elem]
65
+ end
63
66
  else
64
- result[calculated_key] = value
67
+ result << [calculated_key, value]
65
68
  end
66
69
  end
67
70
  result
@@ -88,15 +91,15 @@ module RestClient
88
91
  end
89
92
 
90
93
  def short_inspect
91
- (size > 100 ? "#{size} byte length" : inspect)
94
+ (size > 100 ? "#{size} byte(s) length" : inspect)
92
95
  end
93
96
 
94
97
  end
95
98
 
96
99
  class UrlEncoded < Base
97
100
  def build_stream(params = nil)
98
- @stream = StringIO.new(flatten_params(params).map do |k, v|
99
- "#{k}=#{escape(v)}"
101
+ @stream = StringIO.new(flatten_params(params).collect do |entry|
102
+ "#{entry[0]}=#{escape(entry[1])}"
100
103
  end.join("&"))
101
104
  @stream.seek(0)
102
105
  end
@@ -116,9 +119,9 @@ module RestClient
116
119
  @stream.write(b + EOL)
117
120
 
118
121
  if params.is_a? Hash
119
- x = flatten_params(params).to_a
122
+ x = flatten_params(params)
120
123
  else
121
- x = params.to_a
124
+ x = params
122
125
  end
123
126
 
124
127
  last_index = x.length - 1
@@ -1,5 +1,3 @@
1
- require File.dirname(__FILE__) + '/mixin/response'
2
-
3
1
  module RestClient
4
2
  # The response from RestClient on a raw request looks like a string, but is
5
3
  # actually one of these. 99% of the time you're making a rest call all you
@@ -11,13 +9,12 @@ module RestClient
11
9
  # In addition, if you do not use the response as a string, you can access
12
10
  # a Tempfile object at res.file, which contains the path to the raw
13
11
  # downloaded request body.
14
- class RawResponse
15
- include RestClient::Mixin::Response
12
+ class RawResponse < AbstractResponse
16
13
 
17
14
  attr_reader :file
18
15
 
19
- def initialize(tempfile, net_http_res)
20
- @net_http_res = net_http_res
16
+ def initialize tempfile, net_http_res, args
17
+ super net_http_res, args
21
18
  @file = tempfile
22
19
  end
23
20
 
@@ -26,5 +23,9 @@ module RestClient
26
23
  @file.read
27
24
  end
28
25
 
26
+ def size
27
+ File.size file
28
+ end
29
+
29
30
  end
30
31
  end
@@ -21,11 +21,10 @@ module RestClient
21
21
  # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
22
22
  class Request
23
23
 
24
- attr_reader :method, :url, :payload, :headers, :processed_headers,
25
- :cookies, :user, :password, :timeout, :open_timeout,
26
- :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
27
- :raw_response
28
-
24
+ attr_reader :method, :url, :headers, :cookies,
25
+ :payload, :user, :password, :timeout,
26
+ :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
27
+ :ssl_client_key, :ssl_ca_file, :processed_headers, :args
29
28
 
30
29
  def self.execute(args, &block)
31
30
  new(args).execute &block
@@ -48,20 +47,10 @@ module RestClient
48
47
  @ssl_ca_file = args[:ssl_ca_file] || nil
49
48
  @tf = nil # If you are a raw request, this is your tempfile
50
49
  @processed_headers = make_headers headers
50
+ @args = args
51
51
  end
52
52
 
53
53
  def execute &block
54
- execute_inner &block
55
- rescue Redirect => e
56
- @processed_headers.delete "Content-Length"
57
- @processed_headers.delete "Content-Type"
58
- @url = e.url
59
- @method = :get
60
- @payload = nil
61
- execute &block
62
- end
63
-
64
- def execute_inner &block
65
54
  uri = parse_url_with_auth(url)
66
55
  transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, &block
67
56
  end
@@ -152,6 +141,10 @@ module RestClient
152
141
  net.read_timeout = @timeout if @timeout
153
142
  net.open_timeout = @open_timeout if @open_timeout
154
143
 
144
+ RestClient.before_execution_procs.each do |block|
145
+ block.call(req, args)
146
+ end
147
+
155
148
  log_request
156
149
 
157
150
  net.start do |http|
@@ -197,32 +190,20 @@ module RestClient
197
190
  http_response
198
191
  end
199
192
 
200
- def process_result res
193
+ def process_result res, &block
201
194
  if @raw_response
202
195
  # We don't decode raw requests
203
- response = RawResponse.new(@tf, res)
196
+ response = RawResponse.new(@tf, res, args)
204
197
  else
205
- response = Response.new(Request.decode(res['content-encoding'], res.body), res)
198
+ response = Response.new(Request.decode(res['content-encoding'], res.body), res, args)
206
199
  end
207
200
 
208
- code = res.code.to_i
209
-
210
- if (301..303).include? code
211
- url = res.header['Location']
212
-
213
- if url !~ /^http/
214
- uri = URI.parse(@url)
215
- uri.path = "/#{url}".squeeze('/')
216
- url = uri.to_s
217
- end
218
- raise Redirect, url
201
+ if block_given?
202
+ block.call response, &block
219
203
  else
220
- if block_given?
221
- yield response
222
- else
223
- response.return!
224
- end
204
+ response.return! &block
225
205
  end
206
+
226
207
  end
227
208
 
228
209
  def self.decode content_encoding, body
@@ -1,19 +1,43 @@
1
- require File.dirname(__FILE__) + '/mixin/response'
2
-
3
1
  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]
2
+
3
+ # A Response from RestClient, you can access the response body, the code or the headers.
9
4
  #
10
- class Response < String
5
+ class Response < AbstractResponse
6
+
7
+ attr_reader :body
8
+
9
+ def initialize body, net_http_res, args
10
+ super net_http_res, args
11
+ @body = body || ""
12
+ end
11
13
 
12
- include RestClient::Mixin::Response
14
+ def method_missing symbol, *args
15
+ if body.respond_to? symbol
16
+ warn "[warning] The Response is no more a String, please update your code"
17
+ body.send symbol, *args
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def == o
24
+ if super
25
+ true
26
+ else
27
+ equal_body = (body == o)
28
+ if equal_body
29
+ warn "[warning] The Response is no more a String, please update your code"
30
+ end
31
+ equal_body
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ body.to_s
37
+ end
13
38
 
14
- def initialize(string, net_http_res)
15
- @net_http_res = net_http_res
16
- super(string || "")
39
+ def size
40
+ body.size
17
41
  end
18
42
 
19
43
  end
@@ -1,18 +1,9 @@
1
- require File.dirname(__FILE__) + '/../base'
1
+ require File.dirname(__FILE__) + '/base'
2
2
 
3
- class MockResponse
4
- include RestClient::Mixin::Response
5
-
6
- def initialize(body, res)
7
- @net_http_res = res
8
- @body = body
9
- end
10
- end
11
-
12
- describe RestClient::Mixin::Response do
3
+ describe RestClient::AbstractResponse do
13
4
  before do
14
5
  @net_http_res = mock('net http response')
15
- @response = MockResponse.new('abc', @net_http_res)
6
+ @response = RestClient::AbstractResponse.new(@net_http_res, {})
16
7
  end
17
8
 
18
9
  it "fetches the numeric response code" do
@@ -10,14 +10,14 @@ describe RestClient do
10
10
  stub_request(:get, "www.example.com").to_return(:body => body, :status => 200)
11
11
  response = RestClient.get "www.example.com"
12
12
  response.code.should == 200
13
- response.should == body
13
+ response.body.should == body
14
14
  end
15
15
 
16
16
  it "a simple request with gzipped content" do
17
17
  stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } )
18
18
  response = RestClient.get "www.example.com"
19
19
  response.code.should == 200
20
- response.should == "i'm gziped\n"
20
+ response.body.should == "i'm gziped\n"
21
21
  end
22
22
 
23
23
  it "a 404" do
@@ -29,7 +29,7 @@ describe RestClient do
29
29
  rescue RestClient::ResourceNotFound => e
30
30
  e.http_code.should == 404
31
31
  e.response.code.should == 404
32
- e.response.should == body
32
+ e.response.body.should == body
33
33
  e.http_body.should == body
34
34
  end
35
35
  end
@@ -10,6 +10,8 @@ describe RestClient::Payload do
10
10
  it "should form properly encoded params" do
11
11
  RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s.
12
12
  should == "foo=bar"
13
+ ["foo=bar&baz=qux", "baz=qux&foo=bar"].should include(
14
+ RestClient::Payload::UrlEncoded.new({:foo => 'bar', :baz => 'qux'}).to_s)
13
15
  end
14
16
 
15
17
  it "should properly handle hashes as parameter" do
@@ -26,6 +28,13 @@ describe RestClient::Payload do
26
28
  should == "foo[bar]=baz"
27
29
  end
28
30
 
31
+ it "should properyl handle arrays as repeated parameters" do
32
+ RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s.
33
+ should == "foo=bar"
34
+ RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s.
35
+ should == "foo=bar&foo=baz"
36
+ end
37
+
29
38
  end
30
39
 
31
40
  context "A multipart Payload" do
@@ -4,7 +4,7 @@ describe RestClient::RawResponse do
4
4
  before do
5
5
  @tf = mock("Tempfile", :read => "the answer is 42", :open => true)
6
6
  @net_http_res = mock('net http response')
7
- @response = RestClient::RawResponse.new(@tf, @net_http_res)
7
+ @response = RestClient::RawResponse.new(@tf, @net_http_res, {})
8
8
  end
9
9
 
10
10
  it "behaves like string" do
@@ -1,5 +1,8 @@
1
1
  require File.dirname(__FILE__) + '/base'
2
2
 
3
+ require 'webmock/rspec'
4
+ include WebMock
5
+
3
6
  describe RestClient::Request do
4
7
  before do
5
8
  @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
@@ -45,7 +48,8 @@ describe RestClient::Request do
45
48
  res.stub!(:code).and_return("200")
46
49
  res.stub!(:body).and_return('body')
47
50
  res.stub!(:[]).with('content-encoding').and_return(nil)
48
- @request.process_result(res).should == 'body'
51
+ @request.process_result(res).body.should == 'body'
52
+ @request.process_result(res).to_s.should == 'body'
49
53
  end
50
54
 
51
55
  it "doesn't classify successful requests as failed" do
@@ -153,7 +157,7 @@ describe RestClient::Request do
153
157
  @request.should_receive(:net_http_request_class).with(:put).and_return(klass)
154
158
  klass.should_receive(:new).and_return('result')
155
159
  @request.should_receive(:transmit).with(@uri, 'result', kind_of(RestClient::Payload::Base))
156
- @request.execute_inner
160
+ @request.execute
157
161
  end
158
162
 
159
163
  it "transmits the request with Net::HTTP" do
@@ -225,11 +229,6 @@ describe RestClient::Request do
225
229
  lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
226
230
  end
227
231
 
228
- it "execute calls execute_inner" do
229
- @request.should_receive(:execute_inner)
230
- @request.execute
231
- end
232
-
233
232
  it "class method execute wraps constructor" do
234
233
  req = mock("rest request")
235
234
  RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
@@ -237,39 +236,6 @@ describe RestClient::Request do
237
236
  RestClient::Request.execute(1 => 2)
238
237
  end
239
238
 
240
- describe "redirection" do
241
- it "raises a Redirect with the new location when the response is in the 30x range" do
242
- res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource'}, :[] => ['content-encoding' => ''], :body => '' )
243
- lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
244
- end
245
-
246
- it "handles redirects with relative paths" do
247
- res = mock('response', :code => '301', :header => { 'Location' => 'index' }, :[] => ['content-encoding' => ''], :body => '' )
248
- lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
249
- end
250
-
251
- it "handles redirects with absolute paths" do
252
- @request.instance_variable_set('@url', 'http://some/place/else')
253
- res = mock('response', :code => '301', :header => { 'Location' => '/index' }, :[] => ['content-encoding' => ''], :body => '' )
254
- lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
255
- end
256
-
257
- it "uses GET and clears payload and removes possible harmful headers when following 30x redirects" do
258
- url = "http://example.com/redirected"
259
-
260
- @request.should_receive(:execute_inner).once.ordered.and_raise(RestClient::Redirect.new(url))
261
-
262
- @request.should_receive(:execute_inner).once.ordered do
263
- @request.processed_headers.should_not have_key("Content-Length")
264
- @request.url.should == url
265
- @request.method.should == :get
266
- @request.payload.should be_nil
267
- end
268
-
269
- @request.execute
270
- end
271
- end
272
-
273
239
  describe "exception" do
274
240
  it "raises Unauthorized when the response is 401" do
275
241
  res = mock('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' )
@@ -324,8 +290,8 @@ describe RestClient::Request do
324
290
  it "logs a post request with a large payload" do
325
291
  log = RestClient.log = []
326
292
  RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request
327
- ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"' + "\n",
328
- 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"' + "\n"].should include(log[0])
293
+ ['RestClient.post "http://url", 1000 byte(s) length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"' + "\n",
294
+ 'RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"' + "\n"].should include(log[0])
329
295
  end
330
296
 
331
297
  it "logs input headers as a hash" do
@@ -1,17 +1,20 @@
1
1
  require File.dirname(__FILE__) + '/base'
2
2
 
3
+ require 'webmock/rspec'
4
+ include WebMock
5
+
3
6
  describe RestClient::Response do
4
7
  before do
5
8
  @net_http_res = mock('net http response', :to_hash => {"Status" => ["200 OK"]})
6
- @response = RestClient::Response.new('abc', @net_http_res)
9
+ @response = RestClient::Response.new('abc', @net_http_res, {})
7
10
  end
8
11
 
9
12
  it "behaves like string" do
10
- @response.should == 'abc'
13
+ @response.should.to_s == 'abc'
11
14
  end
12
15
 
13
16
  it "accepts nil strings and sets it to empty for the case of HEAD" do
14
- RestClient::Response.new(nil, @net_http_res).should == ""
17
+ RestClient::Response.new(nil, @net_http_res, {}).should.to_s == ""
15
18
  end
16
19
 
17
20
  it "test headers and raw headers" do
@@ -22,14 +25,14 @@ describe RestClient::Response do
22
25
  describe "cookie processing" do
23
26
  it "should correctly deal with one Set-Cookie header with one cookie inside" do
24
27
  net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]})
25
- response = RestClient::Response.new('abc', net_http_res)
28
+ response = RestClient::Response.new('abc', net_http_res, {})
26
29
  response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]
27
30
  response.cookies.should == { "main_page" => "main_page_no_rewrite" }
28
31
  end
29
32
 
30
33
  it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do
31
34
  net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]})
32
- response = RestClient::Response.new('abc', net_http_res)
35
+ response = RestClient::Response.new('abc', net_http_res, {})
33
36
  response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]
34
37
  response.cookies.should == {
35
38
  "main_page" => "main_page_no_rewrite",
@@ -40,7 +43,7 @@ describe RestClient::Response do
40
43
 
41
44
  it "should correctly deal with multiple cookies [one Set-Cookie header with multiple cookies]" do
42
45
  net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT, remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT, user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]})
43
- response = RestClient::Response.new('abc', net_http_res)
46
+ response = RestClient::Response.new('abc', net_http_res, {})
44
47
  response.cookies.should == {
45
48
  "main_page" => "main_page_no_rewrite",
46
49
  "remember_me" => "",
@@ -53,20 +56,75 @@ describe RestClient::Response do
53
56
  it "should return itself for normal codes" do
54
57
  (200..206).each do |code|
55
58
  net_http_res = mock('net http response', :code => '200')
56
- response = RestClient::Response.new('abc', net_http_res)
59
+ response = RestClient::Response.new('abc', net_http_res, {})
57
60
  response.return!
58
61
  end
59
62
  end
60
63
 
61
64
  it "should throw an exception for other codes" do
62
65
  RestClient::Exceptions::EXCEPTIONS_MAP.each_key do |code|
63
- net_http_res = mock('net http response', :code => code.to_i)
64
- response = RestClient::Response.new('abc', net_http_res)
65
- lambda { response.return!}.should raise_error
66
+ unless (200..206).include? code
67
+ net_http_res = mock('net http response', :code => code.to_i)
68
+ response = RestClient::Response.new('abc', net_http_res, {})
69
+ lambda { response.return!}.should raise_error
70
+ end
66
71
  end
67
72
  end
68
73
 
69
74
  end
70
75
 
76
+ describe "redirection" do
77
+
78
+ it "follows a redirection when the request is a get" do
79
+ stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
80
+ stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
81
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should == 'Foo'
82
+ end
83
+
84
+ it "doesn't follow a redirection when the request is a post" do
85
+ net_http_res = mock('net http response', :code => 301)
86
+ response = RestClient::Response.new('abc', net_http_res, {:method => :post})
87
+ lambda { response.return!}.should raise_error(RestClient::MovedPermanently)
88
+ end
89
+
90
+ it "doesn't follow a redirection when the request is a put" do
91
+ net_http_res = mock('net http response', :code => 301)
92
+ response = RestClient::Response.new('abc', net_http_res, {:method => :put})
93
+ lambda { response.return!}.should raise_error(RestClient::MovedPermanently)
94
+ end
95
+
96
+ it "follows a redirection when the request is a post and result is a 303" do
97
+ stub_request(:put, 'http://some/resource').to_return(:body => '', :status => 303, :headers => {'Location' => 'http://new/resource'})
98
+ stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
99
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body.should == 'Foo'
100
+ end
101
+
102
+ it "follows a redirection when the request is a head" do
103
+ stub_request(:head, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
104
+ stub_request(:head, 'http://new/resource').to_return(:body => 'Foo')
105
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body.should == 'Foo'
106
+ end
107
+
108
+ it "handles redirects with relative paths" do
109
+ stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index'})
110
+ stub_request(:get, 'http://some/index').to_return(:body => 'Foo')
111
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should == 'Foo'
112
+ end
113
+
114
+ it "handles redirects with relative path and query string" do
115
+ stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index?q=1'})
116
+ stub_request(:get, 'http://some/index?q=1').to_return(:body => 'Foo')
117
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should == 'Foo'
118
+ end
119
+
120
+ it "follow a redirection when the request is a get and the response is in the 30x range" do
121
+ stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
122
+ stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
123
+ RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should == 'Foo'
124
+ end
125
+
126
+
127
+ end
128
+
71
129
 
72
130
  end
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ prerelease: true
5
+ segments:
6
+ - 1
7
+ - 4
8
+ - 0
9
+ - a
10
+ version: 1.4.0.a
5
11
  platform: ruby
6
12
  authors:
7
13
  - Adam Wiggins
@@ -10,19 +16,22 @@ autorequire:
10
16
  bindir: bin
11
17
  cert_chain: []
12
18
 
13
- date: 2010-01-25 00:00:00 +01:00
19
+ date: 2010-02-22 00:00:00 +01:00
14
20
  default_executable: restclient
15
21
  dependencies:
16
22
  - !ruby/object:Gem::Dependency
17
23
  name: mime-types
18
- type: :runtime
19
- version_requirement:
20
- version_requirements: !ruby/object:Gem::Requirement
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
21
26
  requirements:
22
27
  - - ">="
23
28
  - !ruby/object:Gem::Version
29
+ segments:
30
+ - 1
31
+ - 16
24
32
  version: "1.16"
25
- version:
33
+ type: :runtime
34
+ version_requirements: *id001
26
35
  description: "A simple Simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
27
36
  email: rest.client@librelist.com
28
37
  executables:
@@ -40,7 +49,7 @@ files:
40
49
  - lib/rest_client.rb
41
50
  - lib/restclient.rb
42
51
  - lib/restclient/exceptions.rb
43
- - lib/restclient/mixin/response.rb
52
+ - lib/restclient/abstract_response.rb
44
53
  - lib/restclient/net_http_ext.rb
45
54
  - lib/restclient/payload.rb
46
55
  - lib/restclient/raw_response.rb
@@ -51,7 +60,7 @@ files:
51
60
  - spec/exceptions_spec.rb
52
61
  - spec/integration_spec.rb
53
62
  - spec/master_shake.jpg
54
- - spec/mixin/response_spec.rb
63
+ - spec/abstract_response_spec.rb
55
64
  - spec/payload_spec.rb
56
65
  - spec/raw_response_spec.rb
57
66
  - spec/request_spec.rb
@@ -72,18 +81,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
81
  requirements:
73
82
  - - ">="
74
83
  - !ruby/object:Gem::Version
84
+ segments:
85
+ - 0
75
86
  version: "0"
76
- version:
77
87
  required_rubygems_version: !ruby/object:Gem::Requirement
78
88
  requirements:
79
89
  - - ">="
80
90
  - !ruby/object:Gem::Version
91
+ segments:
92
+ - 0
81
93
  version: "0"
82
- version:
83
94
  requirements: []
84
95
 
85
96
  rubyforge_project: rest-client
86
- rubygems_version: 1.3.5
97
+ rubygems_version: 1.3.6
87
98
  signing_key:
88
99
  specification_version: 3
89
100
  summary: Simple REST client for Ruby, inspired by microframework syntax for specifying actions.
@@ -91,7 +102,7 @@ test_files:
91
102
  - spec/base.rb
92
103
  - spec/exceptions_spec.rb
93
104
  - spec/integration_spec.rb
94
- - spec/mixin/response_spec.rb
105
+ - spec/abstract_response_spec.rb
95
106
  - spec/payload_spec.rb
96
107
  - spec/raw_response_spec.rb
97
108
  - spec/request_spec.rb
@@ -1,64 +0,0 @@
1
- module RestClient
2
- module Mixin
3
- module Response
4
- attr_reader :net_http_res
5
-
6
- # HTTP status code
7
- def code
8
- @code ||= @net_http_res.code.to_i
9
- end
10
-
11
- # A hash of the headers, beautified with symbols and underscores.
12
- # e.g. "Content-type" will become :content_type.
13
- def headers
14
- @headers ||= self.class.beautify_headers(@net_http_res.to_hash)
15
- end
16
-
17
- # The raw headers.
18
- def raw_headers
19
- @raw_headers ||= @net_http_res.to_hash
20
- end
21
-
22
- # Hash of cookies extracted from response headers
23
- def cookies
24
- @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content|
25
- # correctly parse comma-separated cookies containing HTTP dates (which also contain a comma)
26
- cookie_content.split(/,\s*/).inject([""]) { |array, blob|
27
- blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob)
28
- array
29
- }.each do |cookie|
30
- next if cookie.empty?
31
- key, *val = cookie.split(";").first.split("=")
32
- out[key] = val.join("=")
33
- end
34
- out
35
- end
36
- end
37
-
38
- # Return the default behavior corresponding to the response code:
39
- # the response itself for code in 200..206 and an exception in other cases
40
- def return!
41
- if (200..206).include? code
42
- self
43
- elsif Exceptions::EXCEPTIONS_MAP[code]
44
- raise Exceptions::EXCEPTIONS_MAP[code], self
45
- else
46
- raise RequestFailed, self
47
- end
48
- end
49
-
50
- def self.included(receiver)
51
- receiver.extend(RestClient::Mixin::Response::ClassMethods)
52
- end
53
-
54
- module ClassMethods
55
- def beautify_headers(headers)
56
- headers.inject({}) do |out, (key, value)|
57
- out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first
58
- out
59
- end
60
- end
61
- end
62
- end
63
- end
64
- end