require 'active_resource/connection' require 'cgi' require 'set' module ActiveResource class Base # The logger for logging diagnostic and trace information during ARes # calls. cattr_accessor :logger class << self def site if defined?(@site) @site elsif superclass != Object and superclass.site superclass.site.dup.freeze end end def site=(site) @site = create_site_uri_from(site) @connection = nil @site end def connection(refresh = false) @connection = Connection.new(site) if refresh || @connection.nil? @connection end attr_accessor_with_default(:element_name) { to_s.underscore } attr_accessor_with_default(:collection_name) { element_name.pluralize } attr_accessor_with_default(:primary_key, 'id') def prefix(options={}) default = site.path default << '/' unless default[-1..-1] == '/' self.prefix = default prefix(options) end def prefix=(value = '/') @prefix_parameters = Set.new prefix_call = value.gsub(/:\w+/) do |key| @prefix_parameters << key[1..-1].to_sym "\#{options[#{key}]}" end method_decl = %(def prefix(options={}) "#{prefix_call}" end) instance_eval method_decl, __FILE__, __LINE__ rescue logger.error "Couldn't set prefix: #{$!}\n #{method_decl}" raise end alias_method :set_prefix, :prefix= alias_method :set_element_name, :element_name= alias_method :set_collection_name, :collection_name= def element_path(id, options = {}) "#{prefix(options)}#{collection_name}/#{id}.xml#{query_string(options)}" end def collection_path(options = {}) "#{prefix(options)}#{collection_name}.xml#{query_string(options)}" end alias_method :set_primary_key, :primary_key= # Person.find(1) # => GET /people/1.xml # StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml def find(*arguments) scope = arguments.slice!(0) options = arguments.slice!(0) || {} case scope when :all then find_every(options) when :first then find_every(options).first else find_single(scope, options) end end def delete(id) connection.delete(element_path(id)) end private def find_every(options) collection = connection.get(collection_path(options)) || [] collection.collect! { |element| new(element, options) } end # { :person => person1 } def find_single(scope, options) new(connection.get(element_path(scope, options)), options) end def create_site_uri_from(site) site.is_a?(URI) ? site.dup : URI.parse(site) end def query_string(options) # Omit parameters which appear in the URI path. query_params = options.reject { |key, value| @prefix_parameters.include?(key) } # Accumulate a list of escaped key=value pairs for the given parameters. pairs = [] query_params.each do |key, value| key = CGI.escape(key.to_s) # a => b becomes a=b # a => [b, c] becomes a[]=b&a[]=c case value when Array value.each { |val| pairs << "#{key}[]=#{CGI.escape(val.to_s)}" } else pairs << "#{key}=#{CGI.escape(value.to_s)}" end end "?#{pairs * '&'}" unless pairs.empty? end end attr_accessor :attributes attr_accessor :prefix_options def initialize(attributes = {}, prefix_options = {}) @attributes = {} self.load attributes @prefix_options = prefix_options end def new? id.nil? end def id attributes[self.class.primary_key] end def id=(id) attributes[self.class.primary_key] = id end # True if and only if +other+ is the same object or is an instance of the same class, is not new?, and has the same id. def ==(other) other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id) end # Delegates to == def eql?(other) self == other end # Delegates to id in order to allow two resources of the same type and id to work with something like: # [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)] def hash id.hash end def save new? ? create : update end def destroy connection.delete(element_path) end def to_xml(options={}) attributes.to_xml({:root => self.class.element_name}.merge(options)) end # Reloads the attributes of this object from the remote web service. def reload self.load self.class.find(id, @prefix_options).attributes end # Manually load attributes from a hash. Recursively loads collections of # resources. def load(attributes) raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash) attributes.each do |key, value| @attributes[key.to_s] = case value when Array resource = find_or_create_resource_for_collection(key) value.map { |attrs| resource.new(attrs) } when Hash resource = find_or_create_resource_for(key) resource.new(value) when ActiveResource::Base value.class.new(value.attributes) else value.dup rescue value end end self end protected def connection(refresh = false) self.class.connection(refresh) end def update connection.put(element_path, to_xml) true end def create resp = connection.post(collection_path, to_xml) self.id = id_from_response(resp) true end # takes a response from a typical create post and pulls the ID out def id_from_response(response) response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] end def element_path(options = nil) self.class.element_path(id, options || prefix_options) end def collection_path(options = nil) self.class.collection_path(options || prefix_options) end private def find_or_create_resource_for_collection(name) find_or_create_resource_for(name.to_s.singularize) end def find_or_create_resource_for(name) resource_name = name.to_s.camelize resource_name.constantize rescue NameError resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base)) resource.prefix = self.class.prefix resource.site = self.class.site resource end def method_missing(method_symbol, *arguments) method_name = method_symbol.to_s case method_name.last when "=" attributes[method_name.first(-1)] = arguments.first when "?" attributes[method_name.first(-1)] == true else attributes.has_key?(method_name) ? attributes[method_name] : super end end end end