diff options
Diffstat (limited to 'activeresource/lib')
-rw-r--r-- | activeresource/lib/active_resource/base.rb | 136 | ||||
-rw-r--r-- | activeresource/lib/active_resource/connection.rb | 42 | ||||
-rw-r--r-- | activeresource/lib/active_resource/exceptions.rb | 11 | ||||
-rw-r--r-- | activeresource/lib/active_resource/validations.rb | 66 |
4 files changed, 229 insertions, 26 deletions
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 3d58660ffc..e1f221bd3e 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -110,6 +110,8 @@ module ActiveResource # # Many REST APIs will require authentication, usually in the form of basic # HTTP authentication. Authentication can be specified by: + # + # === HTTP Basic Authentication # * putting the credentials in the URL for the +site+ variable. # # class Person < ActiveResource::Base @@ -130,6 +132,19 @@ module ActiveResource # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses # as usernames. In those situations you should use the separate user and password option. # + # === Certificate Authentication + # + # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options. + # + # class Person < ActiveResource::Base + # self.site = "https://secure.api.people.com/" + # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file)) + # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)), + # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs", + # :verify_mode => OpenSSL::SSL::VERIFY_PEER} + # end + # + # # == Errors & Validation # # Error handling and validation is handled in much the same manner as you're used to seeing in @@ -156,6 +171,7 @@ module ActiveResource # * 404 - ActiveResource::ResourceNotFound # * 405 - ActiveResource::MethodNotAllowed # * 409 - ActiveResource::ResourceConflict + # * 410 - ActiveResource::ResourceGone # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors) # * 401..499 - ActiveResource::ClientError # * 500..599 - ActiveResource::ServerError @@ -176,7 +192,7 @@ module ActiveResource # # Active Resource supports validations on resources and will return errors if any of these validations fail # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by - # a response code of <tt>422</tt> and an XML representation of the validation errors. The save operation will + # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question. # # ryan = Person.find(1) @@ -185,10 +201,14 @@ module ActiveResource # # # When # # PUT http://api.people.com:3000/people/1.xml + # # or + # # PUT http://api.people.com:3000/people/1.json # # is requested with invalid values, the response is: # # # # Response (422): # # <errors type="array"><error>First cannot be empty</error></errors> + # # or + # # {"errors":["First cannot be empty"]} # # # # ryan.errors.invalid?(:first) # => true @@ -349,6 +369,31 @@ module ActiveResource end end + # Options that will get applied to an SSL connection. + # + # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate + # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contrain several CA certificates. + # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format. + # * <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) + # * <tt>:verify_callback</tt> - The verify callback for the server certification verification. + # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification. + # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate. + # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds. + def ssl_options=(opts={}) + @connection = nil + @ssl_options = opts + end + + # Returns the SSL options hash. + def ssl_options + if defined?(@ssl_options) + @ssl_options + elsif superclass != Object && superclass.ssl_options + superclass.ssl_options + end + end + # An instance of ActiveResource::Connection that is the base \connection to the remote service. # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request # or not (defaults to <tt>false</tt>). @@ -359,6 +404,7 @@ module ActiveResource @connection.user = user if user @connection.password = password if password @connection.timeout = timeout if timeout + @connection.ssl_options = ssl_options if ssl_options @connection else superclass.connection @@ -546,6 +592,19 @@ module ActiveResource # # StreetAddress.find(1, :params => { :person_id => 1 }) # # => GET /people/1/street_addresses/1.xml + # + # == Failure or missing data + # A failure to find the requested object raises a ResourceNotFound + # exception if the find was called with an id. + # With any other scope, find returns nil when no data is returned. + # + # Person.find(1) + # # => raises ResourcenotFound + # + # Person.find(:all) + # Person.find(:first) + # Person.find(:last) + # # => nil def find(*arguments) scope = arguments.slice!(0) options = arguments.slice!(0) || {} @@ -559,6 +618,28 @@ module ActiveResource end end + + # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass + # in all the same arguments to this method as you can to + # <tt>find(:first)</tt>. + def first(*args) + find(:first, *args) + end + + # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass + # in all the same arguments to this method as you can to + # <tt>find(:last)</tt>. + def last(*args) + find(:last, *args) + end + + # This is an alias for find(:all). You can pass in all the same + # arguments to this method as you can to <tt>find(:all)</tt> + def all(*args) + find(:all, *args) + end + + # Deletes the resources with the ID in the +id+ parameter. # # ==== Options @@ -592,23 +673,29 @@ module ActiveResource response.code.to_i == 200 end # id && !find_single(id, options).nil? - rescue ActiveResource::ResourceNotFound + rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone false end private # Find every resource def find_every(options) - case from = options[:from] - when Symbol - instantiate_collection(get(from, options[:params])) - when String - path = "#{from}#{query_string(options[:params])}" - instantiate_collection(connection.get(path, headers) || []) - else - prefix_options, query_options = split_options(options[:params]) - path = collection_path(prefix_options, query_options) - instantiate_collection( (connection.get(path, headers) || []), prefix_options ) + begin + case from = options[:from] + when Symbol + instantiate_collection(get(from, options[:params])) + when String + path = "#{from}#{query_string(options[:params])}" + instantiate_collection(connection.get(path, headers) || []) + else + prefix_options, query_options = split_options(options[:params]) + path = collection_path(prefix_options, query_options) + instantiate_collection( (connection.get(path, headers) || []), prefix_options ) + end + rescue ActiveResource::ResourceNotFound + # Swallowing ResourceNotFound exceptions and return nil - as per + # ActiveRecord. + nil end end @@ -835,6 +922,23 @@ module ActiveResource def save new? ? create : update end + + # Saves the resource. + # + # If the resource is new, it is created via +POST+, otherwise the + # existing resource is updated via +PUT+. + # + # With <tt>save!</tt> validations always run. If any of them fail + # ActiveResource::ResourceInvalid gets raised, and nothing is POSTed to + # the remote system. + # See ActiveResource::Validations for more information. + # + # There's a series of callbacks associated with <tt>save!</tt>. If any + # of the <tt>before_*</tt> callbacks return +false+ the action is + # cancelled and <tt>save!</tt> raises ActiveResource::ResourceInvalid. + def save! + save || raise(ResourceInvalid.new(self)) + end # Deletes the resource from the remote service. # @@ -985,7 +1089,13 @@ module ActiveResource case value when Array resource = find_or_create_resource_for_collection(key) - value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) } + value.map do |attrs| + if attrs.is_a?(String) || attrs.is_a?(Numeric) + attrs.duplicable? ? attrs.dup : attrs + else + resource.new(attrs) + end + end when Hash resource = find_or_create_resource_for(key) resource.new(value) diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index ef57c1f8b2..9d551f04e7 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -13,10 +13,11 @@ module ActiveResource HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept', :put => 'Content-Type', :post => 'Content-Type', - :delete => 'Accept' + :delete => 'Accept', + :head => 'Accept' } - attr_reader :site, :user, :password, :timeout, :proxy + attr_reader :site, :user, :password, :timeout, :proxy, :ssl_options attr_accessor :format class << self @@ -61,6 +62,11 @@ module ActiveResource @timeout = timeout end + # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'. + def ssl_options=(opts={}) + @ssl_options = opts + end + # Executes a GET request. # Used to get (find) resources. def get(path, headers = {}) @@ -88,7 +94,7 @@ module ActiveResource # Executes a HEAD request. # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers). def head(path, headers = {}) - request(:head, path, build_request_headers(headers)) + request(:head, path, build_request_headers(headers, :head)) end @@ -102,6 +108,8 @@ module ActiveResource handle_response(result) rescue Timeout::Error => e raise TimeoutError.new(e.message) + rescue OpenSSL::SSL::SSLError => e + raise SSLError.new(e.message) end # Handles response and error codes from the remote service. @@ -123,6 +131,8 @@ module ActiveResource raise(MethodNotAllowed.new(response)) when 409 raise(ResourceConflict.new(response)) + when 410 + raise(ResourceGone.new(response)) when 422 raise(ResourceInvalid.new(response)) when 401...500 @@ -149,8 +159,7 @@ module ActiveResource end def configure_http(http) - http.use_ssl = @site.is_a?(URI::HTTPS) - http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl? + http = apply_ssl_options(http) # Net::HTTP timeouts default to 60 seconds. if @timeout @@ -161,6 +170,29 @@ module ActiveResource http end + def apply_ssl_options(http) + return http unless @site.is_a?(URI::HTTPS) + + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + return http unless defined?(@ssl_options) + + http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path] + http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file] + + http.cert = @ssl_options[:cert] if @ssl_options[:cert] + http.key = @ssl_options[:key] if @ssl_options[:key] + + http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store] + http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout] + + http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode] + http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback] + http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth] + + http + end + def default_header @default_header ||= {} end diff --git a/activeresource/lib/active_resource/exceptions.rb b/activeresource/lib/active_resource/exceptions.rb index 5e4b1d4487..0631cdcf9f 100644 --- a/activeresource/lib/active_resource/exceptions.rb +++ b/activeresource/lib/active_resource/exceptions.rb @@ -20,6 +20,14 @@ module ActiveResource def to_s; @message ;end end + # Raised when a OpenSSL::SSL::SSLError occurs. + class SSLError < ConnectionError + def initialize(message) + @message = message + end + def to_s; @message ;end + end + # 3xx Redirection class Redirection < ConnectionError # :nodoc: def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end @@ -43,6 +51,9 @@ module ActiveResource # 409 Conflict class ResourceConflict < ClientError; end # :nodoc: + # 410 Gone + class ResourceGone < ClientError; end # :nodoc: + # 5xx Server Error class ServerError < ConnectionError; end # :nodoc: diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index a2ba224998..d4d282e273 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -7,11 +7,12 @@ module ActiveResource # Active Resource validation is reported to and from this object, which is used by Base#save # to determine whether the object in a valid state to be saved. See usage example in Validations. class Errors < ActiveModel::Errors - # Grabs errors from the XML response. - def from_xml(xml) - clear + # Grabs errors from an array of messages (like ActiveRecord::Validations) + # The second parameter directs the errors cache to be cleared (default) + # or not (by passing true) + def from_array(messages, save_cache = false) + clear unless save_cache humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) } - messages = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue [] messages.each do |message| attr_message = humanized_attributes.keys.detect do |attr_name| if message[0, attr_name.size + 1] == "#{attr_name} " @@ -22,6 +23,18 @@ module ActiveResource self[:base] << message if attr_message.nil? end end + + # Grabs errors from a json response. + def from_json(json, save_cache = false) + array = ActiveSupport::JSON.decode(json)['errors'] rescue [] + from_array array, save_cache + end + + # Grabs errors from an XML response. + def from_xml(xml, save_cache = false) + array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue [] + from_array array, save_cache + end end # Module to support validation and errors with Active Resource objects. The module overrides @@ -46,21 +59,55 @@ module ActiveResource # module Validations extend ActiveSupport::Concern + include ActiveModel::Validations + extend ActiveModel::Validations::ClassMethods included do alias_method_chain :save, :validation end # Validate a resource and save (POST) it to the remote web service. - def save_with_validation - save_without_validation - true + # If any local validations fail - the save (POST) will not be attempted. + def save_with_validation(perform_validation = true) + # clear the remote validations so they don't interfere with the local + # ones. Otherwise we get an endless loop and can never change the + # fields so as to make the resource valid + @remote_errors = nil + if perform_validation && valid? || !perform_validation + save_without_validation + true + else + false + end rescue ResourceInvalid => error - errors.from_xml(error.response.body) + # cache the remote errors because every call to <tt>valid?</tt> clears + # all errors. We must keep a copy to add these back after local + # validations + @remote_errors = error + load_remote_errors(@remote_errors, true) false end + + # Loads the set of remote errors into the object's Errors based on the + # content-type of the error-block received + def load_remote_errors(remote_errors, save_cache = false ) #:nodoc: + case remote_errors.response['Content-Type'] + when 'application/xml' + errors.from_xml(remote_errors.response.body, save_cache) + when 'application/json' + errors.from_json(remote_errors.response.body, save_cache) + end + end + # Checks for errors on an object (i.e., is resource.errors empty?). + # + # Runs all the specified local validations and returns true if no errors + # were added, otherwise false. + # Runs local validations (eg those on your Active Resource model), and + # also any errors returned from the remote system the last time we + # saved. + # Remote errors can only be cleared by trying to re-save the resource. # # ==== Examples # my_person = Person.create(params[:person]) @@ -70,7 +117,10 @@ module ActiveResource # my_person.errors.add('login', 'can not be empty') if my_person.login == '' # my_person.valid? # # => false + # def valid? + super + load_remote_errors(@remote_errors, true) if defined?(@remote_errors) && @remote_errors.present? errors.empty? end |