activeresource 2.3.3 → 2.3.4

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

Potentially problematic release.


This version of activeresource might be problematic. Click here for more details.

data/CHANGELOG CHANGED
@@ -1,7 +1,19 @@
1
- *2.3.3 (July 20, 2009)*
1
+ *2.3.4 (September 4, 2009)*
2
+
3
+ * Add support for errors in JSON format. #1956 [Fabien Jakimowicz]
4
+
5
+ * Recognizes 410 as Resource Gone. #2316 [Jordan Brough, Jatinder Singh]
6
+
7
+ * More thorough SSL support. #2370 [Roy Nicholson]
8
+
9
+ * HTTP proxy support. #2133 [Marshall Huss, Sébastien Dabet]
10
+
11
+
12
+ *2.3.3 (July 12, 2009)*
2
13
 
3
14
  * No changes, just a version bump.
4
15
 
16
+
5
17
  *2.3.2 [Final] (March 15, 2009)*
6
18
 
7
19
  * Nothing new, just included in 2.3.2
data/Rakefile CHANGED
@@ -66,7 +66,7 @@ spec = Gem::Specification.new do |s|
66
66
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
67
67
  end
68
68
 
69
- s.add_dependency('activesupport', '= 2.3.3' + PKG_BUILD)
69
+ s.add_dependency('activesupport', '= 2.3.4' + PKG_BUILD)
70
70
 
71
71
  s.require_path = 'lib'
72
72
  s.autorequire = 'active_resource'
@@ -93,6 +93,8 @@ module ActiveResource
93
93
  #
94
94
  # Many REST APIs will require authentication, usually in the form of basic
95
95
  # HTTP authentication. Authentication can be specified by:
96
+ #
97
+ # === HTTP Basic Authentication
96
98
  # * putting the credentials in the URL for the +site+ variable.
97
99
  #
98
100
  # class Person < ActiveResource::Base
@@ -112,6 +114,19 @@ module ActiveResource
112
114
  #
113
115
  # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
114
116
  # as usernames. In those situations you should use the separate user and password option.
117
+ #
118
+ # === Certificate Authentication
119
+ #
120
+ # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options.
121
+ #
122
+ # class Person < ActiveResource::Base
123
+ # self.site = "https://secure.api.people.com/"
124
+ # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file))
125
+ # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)),
126
+ # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs",
127
+ # :verify_mode => OpenSSL::SSL::VERIFY_PEER}
128
+ # end
129
+ #
115
130
  # == Errors & Validation
116
131
  #
117
132
  # Error handling and validation is handled in much the same manner as you're used to seeing in
@@ -138,6 +153,7 @@ module ActiveResource
138
153
  # * 404 - ActiveResource::ResourceNotFound
139
154
  # * 405 - ActiveResource::MethodNotAllowed
140
155
  # * 409 - ActiveResource::ResourceConflict
156
+ # * 410 - ActiveResource::ResourceGone
141
157
  # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
142
158
  # * 401..499 - ActiveResource::ClientError
143
159
  # * 500..599 - ActiveResource::ServerError
@@ -158,7 +174,7 @@ module ActiveResource
158
174
  #
159
175
  # Active Resource supports validations on resources and will return errors if any these validations fail
160
176
  # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
161
- # a response code of <tt>422</tt> and an XML representation of the validation errors. The save operation will
177
+ # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will
162
178
  # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
163
179
  #
164
180
  # ryan = Person.find(1)
@@ -167,10 +183,14 @@ module ActiveResource
167
183
  #
168
184
  # # When
169
185
  # # PUT http://api.people.com:3000/people/1.xml
186
+ # # or
187
+ # # PUT http://api.people.com:3000/people/1.json
170
188
  # # is requested with invalid values, the response is:
171
189
  # #
172
190
  # # Response (422):
173
191
  # # <errors type="array"><error>First cannot be empty</error></errors>
192
+ # # or
193
+ # # {"errors":["First cannot be empty"]}
174
194
  # #
175
195
  #
176
196
  # ryan.errors.invalid?(:first) # => true
@@ -246,6 +266,22 @@ module ActiveResource
246
266
  end
247
267
  end
248
268
 
269
+ # Gets the \proxy variable if a proxy is required
270
+ def proxy
271
+ # Not using superclass_delegating_reader. See +site+ for explanation
272
+ if defined?(@proxy)
273
+ @proxy
274
+ elsif superclass != Object && superclass.proxy
275
+ superclass.proxy.dup.freeze
276
+ end
277
+ end
278
+
279
+ # Sets the URI of the http proxy to the value in the +proxy+ argument.
280
+ def proxy=(proxy)
281
+ @connection = nil
282
+ @proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy)
283
+ end
284
+
249
285
  # Gets the \user for REST HTTP authentication.
250
286
  def user
251
287
  # Not using superclass_delegating_reader. See +site+ for explanation
@@ -315,15 +351,42 @@ module ActiveResource
315
351
  end
316
352
  end
317
353
 
354
+ # Options that will get applied to an SSL connection.
355
+ #
356
+ # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
357
+ # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate
358
+ # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contrain several CA certificates.
359
+ # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format.
360
+ # * <tt>:verify_mode</tt> - Flags for server the certification verification at begining of SSL/TLS session. (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable)
361
+ # * <tt>:verify_callback</tt> - The verify callback for the server certification verification.
362
+ # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification.
363
+ # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate.
364
+ # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds.
365
+ def ssl_options=(opts={})
366
+ @connection = nil
367
+ @ssl_options = opts
368
+ end
369
+
370
+ # Returns the SSL options hash.
371
+ def ssl_options
372
+ if defined?(@ssl_options)
373
+ @ssl_options
374
+ elsif superclass != Object && superclass.ssl_options
375
+ superclass.ssl_options
376
+ end
377
+ end
378
+
318
379
  # An instance of ActiveResource::Connection that is the base \connection to the remote service.
319
380
  # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
320
381
  # or not (defaults to <tt>false</tt>).
321
382
  def connection(refresh = false)
322
383
  if defined?(@connection) || superclass == Object
323
384
  @connection = Connection.new(site, format) if refresh || @connection.nil?
385
+ @connection.proxy = proxy if proxy
324
386
  @connection.user = user if user
325
387
  @connection.password = password if password
326
388
  @connection.timeout = timeout if timeout
389
+ @connection.ssl_options = ssl_options if ssl_options
327
390
  @connection
328
391
  else
329
392
  superclass.connection
@@ -557,7 +620,7 @@ module ActiveResource
557
620
  response.code.to_i == 200
558
621
  end
559
622
  # id && !find_single(id, options).nil?
560
- rescue ActiveResource::ResourceNotFound
623
+ rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone
561
624
  false
562
625
  end
563
626
 
@@ -611,6 +674,11 @@ module ActiveResource
611
674
  site.is_a?(URI) ? site.dup : URI.parse(site)
612
675
  end
613
676
 
677
+ # Accepts a URI and creates the proxy URI from that.
678
+ def create_proxy_uri_from(proxy)
679
+ proxy.is_a?(URI) ? proxy.dup : URI.parse(proxy)
680
+ end
681
+
614
682
  # contains a set of the current prefix parameters.
615
683
  def prefix_parameters
616
684
  @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
@@ -952,7 +1020,13 @@ module ActiveResource
952
1020
  case value
953
1021
  when Array
954
1022
  resource = find_or_create_resource_for_collection(key)
955
- value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) }
1023
+ value.map do |attrs|
1024
+ if attrs.is_a?(String) || attrs.is_a?(Numeric)
1025
+ attrs.duplicable? ? attrs.dup : attrs
1026
+ else
1027
+ resource.new(attrs)
1028
+ end
1029
+ end
956
1030
  when Hash
957
1031
  resource = find_or_create_resource_for(key)
958
1032
  resource.new(value)
@@ -26,6 +26,14 @@ module ActiveResource
26
26
  def to_s; @message ;end
27
27
  end
28
28
 
29
+ # Raised when a OpenSSL::SSL::SSLError occurs.
30
+ class SSLError < ConnectionError
31
+ def initialize(message)
32
+ @message = message
33
+ end
34
+ def to_s; @message ;end
35
+ end
36
+
29
37
  # 3xx Redirection
30
38
  class Redirection < ConnectionError # :nodoc:
31
39
  def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
@@ -49,6 +57,9 @@ module ActiveResource
49
57
  # 409 Conflict
50
58
  class ResourceConflict < ClientError; end # :nodoc:
51
59
 
60
+ # 410 Gone
61
+ class ResourceGone < ClientError; end # :nodoc:
62
+
52
63
  # 5xx Server Error
53
64
  class ServerError < ConnectionError; end # :nodoc:
54
65
 
@@ -67,10 +78,11 @@ module ActiveResource
67
78
  HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
68
79
  :put => 'Content-Type',
69
80
  :post => 'Content-Type',
70
- :delete => 'Accept'
81
+ :delete => 'Accept',
82
+ :head => 'Accept'
71
83
  }
72
84
 
73
- attr_reader :site, :user, :password, :timeout
85
+ attr_reader :site, :user, :password, :timeout, :proxy, :ssl_options
74
86
  attr_accessor :format
75
87
 
76
88
  class << self
@@ -95,7 +107,12 @@ module ActiveResource
95
107
  @password = URI.decode(@site.password) if @site.password
96
108
  end
97
109
 
98
- # Set user for remote service.
110
+ # Set the proxy for remote service.
111
+ def proxy=(proxy)
112
+ @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
113
+ end
114
+
115
+ # Set the user for remote service.
99
116
  def user=(user)
100
117
  @user = user
101
118
  end
@@ -110,6 +127,11 @@ module ActiveResource
110
127
  @timeout = timeout
111
128
  end
112
129
 
130
+ # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
131
+ def ssl_options=(opts={})
132
+ @ssl_options = opts
133
+ end
134
+
113
135
  # Execute a GET request.
114
136
  # Used to get (find) resources.
115
137
  def get(path, headers = {})
@@ -137,7 +159,7 @@ module ActiveResource
137
159
  # Execute a HEAD request.
138
160
  # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
139
161
  def head(path, headers = {})
140
- request(:head, path, build_request_headers(headers))
162
+ request(:head, path, build_request_headers(headers, :head))
141
163
  end
142
164
 
143
165
 
@@ -151,6 +173,8 @@ module ActiveResource
151
173
  handle_response(result)
152
174
  rescue Timeout::Error => e
153
175
  raise TimeoutError.new(e.message)
176
+ rescue OpenSSL::SSL::SSLError => e
177
+ raise SSLError.new(e.message)
154
178
  end
155
179
 
156
180
  # Handles response and error codes from remote service.
@@ -172,6 +196,8 @@ module ActiveResource
172
196
  raise(MethodNotAllowed.new(response))
173
197
  when 409
174
198
  raise(ResourceConflict.new(response))
199
+ when 410
200
+ raise(ResourceGone.new(response))
175
201
  when 422
176
202
  raise(ResourceInvalid.new(response))
177
203
  when 401...500
@@ -186,10 +212,49 @@ module ActiveResource
186
212
  # Creates new Net::HTTP instance for communication with
187
213
  # remote service and resources.
188
214
  def http
189
- http = Net::HTTP.new(@site.host, @site.port)
190
- http.use_ssl = @site.is_a?(URI::HTTPS)
191
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
192
- http.read_timeout = @timeout if @timeout # If timeout is not set, the default Net::HTTP timeout (60s) is used.
215
+ configure_http(new_http)
216
+ end
217
+
218
+ def new_http
219
+ if @proxy
220
+ Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
221
+ else
222
+ Net::HTTP.new(@site.host, @site.port)
223
+ end
224
+ end
225
+
226
+ def configure_http(http)
227
+ http = apply_ssl_options(http)
228
+
229
+ # Net::HTTP timeouts default to 60 seconds.
230
+ if @timeout
231
+ http.open_timeout = @timeout
232
+ http.read_timeout = @timeout
233
+ end
234
+
235
+ http
236
+ end
237
+
238
+ def apply_ssl_options(http)
239
+ return http unless @site.is_a?(URI::HTTPS)
240
+
241
+ http.use_ssl = true
242
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
243
+ return http unless defined?(@ssl_options)
244
+
245
+ http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
246
+ http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
247
+
248
+ http.cert = @ssl_options[:cert] if @ssl_options[:cert]
249
+ http.key = @ssl_options[:key] if @ssl_options[:key]
250
+
251
+ http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
252
+ http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
253
+
254
+ http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
255
+ http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
256
+ http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
257
+
193
258
  http
194
259
  end
195
260
 
@@ -0,0 +1,66 @@
1
+ module ActiveResource
2
+ class ConnectionError < StandardError # :nodoc:
3
+ attr_reader :response
4
+
5
+ def initialize(response, message = nil)
6
+ @response = response
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
12
+ end
13
+ end
14
+
15
+ # Raised when a Timeout::Error occurs.
16
+ class TimeoutError < ConnectionError
17
+ def initialize(message)
18
+ @message = message
19
+ end
20
+ def to_s; @message ;end
21
+ end
22
+
23
+ # Raised when a OpenSSL::SSL::SSLError occurs.
24
+ class SSLError < ConnectionError
25
+ def initialize(message)
26
+ @message = message
27
+ end
28
+ def to_s; @message ;end
29
+ end
30
+
31
+ # 3xx Redirection
32
+ class Redirection < ConnectionError # :nodoc:
33
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
34
+ end
35
+
36
+ # 4xx Client Error
37
+ class ClientError < ConnectionError; end # :nodoc:
38
+
39
+ # 400 Bad Request
40
+ class BadRequest < ClientError; end # :nodoc
41
+
42
+ # 401 Unauthorized
43
+ class UnauthorizedAccess < ClientError; end # :nodoc
44
+
45
+ # 403 Forbidden
46
+ class ForbiddenAccess < ClientError; end # :nodoc
47
+
48
+ # 404 Not Found
49
+ class ResourceNotFound < ClientError; end # :nodoc:
50
+
51
+ # 409 Conflict
52
+ class ResourceConflict < ClientError; end # :nodoc:
53
+
54
+ # 410 Gone
55
+ class ResourceGone < ClientError; end # :nodoc:
56
+
57
+ # 5xx Server Error
58
+ class ServerError < ConnectionError; end # :nodoc:
59
+
60
+ # 405 Method Not Allowed
61
+ class MethodNotAllowed < ClientError # :nodoc:
62
+ def allowed_methods
63
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
64
+ end
65
+ end
66
+ end
@@ -199,11 +199,10 @@ module ActiveResource
199
199
  alias_method :count, :size
200
200
  alias_method :length, :size
201
201
 
202
- # Grabs errors from the XML response.
203
- def from_xml(xml)
202
+ # Grabs errors from an array of messages (like ActiveRecord::Validations)
203
+ def from_array(messages)
204
204
  clear
205
205
  humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
206
- messages = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
207
206
  messages.each do |message|
208
207
  attr_message = humanized_attributes.keys.detect do |attr_name|
209
208
  if message[0, attr_name.size + 1] == "#{attr_name} "
@@ -214,6 +213,18 @@ module ActiveResource
214
213
  add_to_base message if attr_message.nil?
215
214
  end
216
215
  end
216
+
217
+ # Grabs errors from the json response.
218
+ def from_json(json)
219
+ array = ActiveSupport::JSON.decode(json)['errors'] rescue []
220
+ from_array array
221
+ end
222
+
223
+ # Grabs errors from the XML response.
224
+ def from_xml(xml)
225
+ array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
226
+ from_array array
227
+ end
217
228
  end
218
229
 
219
230
  # Module to support validation and errors with Active Resource objects. The module overrides
@@ -248,7 +259,12 @@ module ActiveResource
248
259
  save_without_validation
249
260
  true
250
261
  rescue ResourceInvalid => error
251
- errors.from_xml(error.response.body)
262
+ case error.response['Content-Type']
263
+ when 'application/xml'
264
+ errors.from_xml(error.response.body)
265
+ when 'application/json'
266
+ errors.from_json(error.response.body)
267
+ end
252
268
  false
253
269
  end
254
270
 
@@ -2,7 +2,7 @@ module ActiveResource
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 2
4
4
  MINOR = 3
5
- TINY = 3
5
+ TINY = 4
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end