rest-client 1.3.1 → 1.4.0.a

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.

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