diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
commit | 9887f238871bb2dd73de6ce8855615bcc5d8d079 (patch) | |
tree | 74fa9ff9524a51701cfa23f708b3f777c65b7fe5 /activeresource | |
parent | aff821508a16245ebc03510ba29c70379718dfb7 (diff) | |
parent | 5214e73850916de3c9127d35a4ecee0424d364a3 (diff) | |
download | rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.gz rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.bz2 rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.zip |
Merge branch 'master' of https://github.com/rails/rails
Diffstat (limited to 'activeresource')
27 files changed, 694 insertions, 445 deletions
diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 8fa1b0b4b3..386bafd7de 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,19 +1,18 @@ -*Rails 3.0.0 [release candidate] (July 26th, 2010)* +*Rails 3.1.0 (unreleased)* -* No material changes - - -*Rails 3.0.0 [beta 4] (June 8th, 2010)* +* No changes -* JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. [Santiago Pastorino] +*Rails 3.0.2 (unreleased)* +* No changes -*Rails 3.0.0 [beta 3] (April 13th, 2010)* +*Rails 3.0.1 (October 15, 2010)* -* No changes +* No Changes, just a version bump. +*Rails 3.0.0 (August 29, 2010)* -*Rails 3.0.0 [beta 1] (February 4, 2010)* +* JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. [Santiago Pastorino] * Add support for errors in JSON format. #1956 [Fabien Jakimowicz] @@ -87,7 +86,7 @@ ActiveResource::HttpMock.respond_to do |mock| mock.get "/people/1.xml", {}, "<person><name>David</name></person>" end - + Now: ActiveResource::HttpMock.respond_to.get "/people/1.xml", {}, "<person><name>David</name></person>" @@ -97,14 +96,14 @@ self.site = "http://app/" self.format = :json end - + person = Person.find(1) # => GET http://app/people/1.json person.name = "David" person.save # => PUT http://app/people/1.json {name: "David"} - + Person.format = :xml person.name = "Mary" - person.save # => PUT http://app/people/1.json <person><name>Mary</name></person> + person.save # => PUT http://app/people/1.json <person><name>Mary</name></person> * Fix reload error when path prefix is used. #8727 [Ian Warshak] @@ -133,7 +132,7 @@ class Project headers['X-Token'] = 'foo' end - + # makes the GET request with the custom X-Token header Project.find(:all) @@ -156,7 +155,7 @@ end assert_kind_of Highrise::Comment, Note.find(1).comments.first - + * Added load_attributes_from_response as a way of loading attributes from other responses than just create [David Heinemeier Hansson] @@ -248,8 +247,8 @@ d * Basic validation support [Rick Olson] - Parses the xml response of ActiveRecord::Errors#to_xml with a similar interface to ActiveRecord::Errors. - + Parses the xml response of ActiveRecord::Errors#to_xml with a similar interface to ActiveRecord::Errors. + render :xml => @person.errors.to_xml, :status => '400 Validation Error' * Deep hashes are converted into collections of resources. [Jeremy Kemper] @@ -267,12 +266,12 @@ d * Add support for specifying prefixes. * Allow overriding of element_name, collection_name, and primary key * Provide simpler HTTP mock interface for testing - + # rails routing code map.resources :posts do |post| post.resources :comments end - + # ActiveResources class Post < ActiveResource::Base self.site = "http://37s.sunrise.i:3000/" @@ -281,7 +280,7 @@ d class Comment < ActiveResource::Base self.site = "http://37s.sunrise.i:3000/posts/:post_id/" end - + @post = Post.find 5 @comments = Comment.find :all, :post_id => @post.id diff --git a/activeresource/MIT-LICENSE b/activeresource/MIT-LICENSE index 1bf965ff1e..216b6e5ba0 100644 --- a/activeresource/MIT-LICENSE +++ b/activeresource/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2006-2010 David Heinemeier Hansson +Copyright (c) 2006-2011 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activeresource/README.rdoc b/activeresource/README.rdoc index 127ac5b4a9..afa25e1676 100644 --- a/activeresource/README.rdoc +++ b/activeresource/README.rdoc @@ -9,7 +9,7 @@ in ActionController::Resources). Active Resource attempts to provide a coherent wrapper object-relational mapping for REST web services. It follows the same philosophy as Active Record, in that one of its prime aims -is to reduce the amount of code needed to map to these resources. This is made possible +is to reduce the amount of code needed to map to these resources. This is made possible by relying on a number of code- and protocol-based conventions that make it easy for Active Resource to infer complex relations and structures. These conventions are outlined in detail in the documentation for ActiveResource::Base. @@ -30,25 +30,25 @@ that inherits from ActiveResource::Base and providing a <tt>site</tt> class vari end Now the Person class is REST enabled and can invoke REST services very similarly to how Active Record invokes -lifecycle methods that operate against a persistent store. +life cycle methods that operate against a persistent store. # Find a person with id = 1 ryan = Person.find(1) - Person.exists?(1) #=> true + Person.exists?(1) # => true As you can see, the methods are quite similar to Active Record's methods for dealing with database records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records). ==== Protocol -Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing +Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing built into Action Controller but will also work with any other REST service that properly implements the protocol. REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification: * GET requests are used for finding and retrieving resources. * POST requests are used to create new resources. * PUT requests are used to update existing resources. -* DELETE requests are used to delete resources. +* DELETE requests are used to delete resources. For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation; for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer]. @@ -69,8 +69,8 @@ for a request for a single element, the XML of that item is expected in response The XML document that is received is used to build a new object of type Person, with each XML element becoming an attribute on the object. - ryan.is_a? Person #=> true - ryan.attribute1 #=> 'value1' + ryan.is_a? Person # => true + ryan.attribute1 # => 'value1' Any complex element (one that contains other elements) becomes its own object: @@ -81,8 +81,8 @@ Any complex element (one that contains other elements) becomes its own object: # for GET http://api.people.com:3000/people/1.xml # ryan = Person.find(1) - ryan.complex #=> <Person::Complex::xxxxx> - ryan.complex.attribute2 #=> 'value2' + ryan.complex # => <Person::Complex::xxxxx> + ryan.complex.attribute2 # => 'value2' Collections can also be requested in a similar fashion @@ -95,9 +95,9 @@ Collections can also be requested in a similar fashion # # for GET http://api.people.com:3000/people.xml # - people = Person.find(:all) - people.first #=> <Person::xxx 'first' => 'Ryan' ...> - people.last #=> <Person::xxx 'first' => 'Jim' ...> + people = Person.all + people.first # => <Person::xxx 'first' => 'Ryan' ...> + people.last # => <Person::xxx 'first' => 'Jim' ...> ==== Create @@ -111,17 +111,17 @@ as the id of the ARes object. # is submitted as the body on # # POST http://api.people.com:3000/people.xml - # + # # when save is called on a new Person object. An empty response is # is expected with a 'Location' header value: # # Response (201): Location: http://api.people.com:3000/people/2 # ryan = Person.new(:first => 'Ryan') - ryan.new? #=> true - ryan.save #=> true - ryan.new? #=> false - ryan.id #=> 2 + ryan.new? # => true + ryan.save # => true + ryan.new? # => false + ryan.id # => 2 ==== Update @@ -139,9 +139,9 @@ server side was successful. # is expected with code (204) # ryan = Person.find(1) - ryan.first #=> 'Ryan' + ryan.first # => 'Ryan' ryan.first = 'Rizzle' - ryan.save #=> true + ryan.save # => true ==== Delete @@ -155,10 +155,10 @@ Destruction of a resource can be invoked as a class and instance method of the r # is expected with response code (200) # ryan = Person.find(1) - ryan.destroy #=> true - ryan.exists? #=> false - Person.delete(2) #=> true - Person.exists?(2) #=> false + ryan.destroy # => true + ryan.exists? # => false + Person.delete(2) # => true + Person.exists?(2) # => false You can find more usage information in the ActiveResource::Base documentation. diff --git a/activeresource/Rakefile b/activeresource/Rakefile index 2145f1017c..42e450da66 100644..100755 --- a/activeresource/Rakefile +++ b/activeresource/Rakefile @@ -1,8 +1,5 @@ -gem 'rdoc', '>= 2.5.9' -require 'rdoc' -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' -require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -22,25 +19,11 @@ namespace :test do ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) activesupport_path = "#{File.dirname(__FILE__)}/../activesupport/lib" Dir.glob("test/**/*_test.rb").all? do |file| - system(ruby, '-w', "-Ilib:test:#{activesupport_path}", file) + sh(ruby, '-w', "-Ilib:test:#{activesupport_path}", file) end or raise "Failures" end end -# Generate the RDoc documentation - -RDoc::Task.new { |rdoc| - rdoc.rdoc_dir = 'doc' - rdoc.title = "Active Resource -- Object-oriented REST services" - rdoc.options << '-f' << 'horo' - rdoc.options << '--main' << 'README.rdoc' - rdoc.options << '--charset' << 'utf-8' - rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') - rdoc.rdoc_files.include('lib/**/*.rb') - rdoc.rdoc_files.exclude('lib/activeresource.rb') -} - - spec = eval(File.read('activeresource.gemspec')) Rake::GemPackageTask.new(spec) do |p| diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb index 3e4a1dd4a1..186865f811 100644 --- a/activeresource/lib/active_resource.rb +++ b/activeresource/lib/active_resource.rb @@ -29,6 +29,7 @@ $:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include require 'active_support' require 'active_model' +require 'active_resource/version' module ActiveResource extend ActiveSupport::Autoload diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 62420725ad..160763779e 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -1,6 +1,6 @@ require 'active_support' require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/module/attr_accessor_with_default' @@ -12,6 +12,7 @@ require 'active_support/core_ext/object/duplicable' require 'set' require 'uri' +require 'active_support/core_ext/uri' require 'active_resource/exceptions' require 'active_resource/connection' require 'active_resource/formats' @@ -21,7 +22,7 @@ require 'active_resource/log_subscriber' module ActiveResource # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application. # - # For an outline of what Active Resource is capable of, see link:files/vendor/rails/activeresource/README.html. + # For an outline of what Active Resource is capable of, see its {README}[link:files/activeresource/README_rdoc.html]. # # == Automated mapping # @@ -35,7 +36,7 @@ module ActiveResource # end # # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and - # you can now use Active Resource's lifecycle methods to manipulate resources. In the case where you already have + # you can now use Active Resource's life cycle methods to manipulate resources. In the case where you already have # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value. # # class PersonResource < ActiveResource::Base @@ -51,7 +52,7 @@ module ActiveResource # end # # - # == Lifecycle methods + # == Life cycle methods # # Active Resource exposes methods for creating, finding, updating, and deleting resources # from REST web services. @@ -70,12 +71,12 @@ module ActiveResource # # ryan.destroy # => true # - # As you can see, these are very similar to Active Record's lifecycle methods for database records. + # As you can see, these are very similar to Active Record's life cycle methods for database records. # You can read more about each of these methods in their respective documentation. # # === Custom REST methods # - # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports + # Since simple CRUD/life cycle methods can't accomplish every task, Active Resource also supports # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>, # <tt>post</tt>, <tt>put</tt> and <tt>\delete</tt> methods where you can specify a custom REST method # name to invoke. @@ -166,6 +167,7 @@ module ActiveResource # # GET http://api.people.com:3000/people/999.xml # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound # + # # <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The # following HTTP response codes will also result in these exceptions: # @@ -194,6 +196,16 @@ module ActiveResource # redirect_to :action => 'new' # end # + # When a GET is requested for a nested resource and you don't provide the prefix_param + # an ActiveResource::MissingPrefixParam will be raised. + # + # class Comment < ActiveResource::Base + # self.site = "http://someip.com/posts/:post_id/" + # end + # + # Comment.find(1) + # # => ActiveResource::MissingPrefixParam: post_id prefix_option is missing + # # === Validation errors # # Active Resource supports validations on resources and will return errors if any of these validations fail @@ -251,6 +263,8 @@ module ActiveResource # The logger for diagnosing and tracing Active Resource calls. cattr_accessor :logger + class_attribute :_format + class << self # Creates a schema for this resource - setting the attributes that are # known prior to fetching an instance from the remote system. @@ -403,8 +417,8 @@ module ActiveResource @site = nil else @site = create_site_uri_from(site) - @user = uri_parser.unescape(@site.user) if @site.user - @password = uri_parser.unescape(@site.password) if @site.password + @user = URI.parser.unescape(@site.user) if @site.user + @password = URI.parser.unescape(@site.password) if @site.password end end @@ -480,13 +494,13 @@ module ActiveResource format = mime_type_reference_or_format.is_a?(Symbol) ? ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format - write_inheritable_attribute(:format, format) + self._format = format connection.format = format if site end # Returns the current format, default is ActiveResource::Formats::XmlFormat. def format - read_inheritable_attribute(:format) || ActiveResource::Formats::XmlFormat + self._format || ActiveResource::Formats::XmlFormat end # Sets the number of seconds after which requests to the REST API should time out. @@ -508,9 +522,9 @@ module ActiveResource # # * <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_file</tt> - Path to a CA certification file in PEM format. The file can contain 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_mode</tt> - Flags for server the certification verification at beginning 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. @@ -577,7 +591,7 @@ module ActiveResource # Default value is <tt>site.path</tt>. def prefix=(value = '/') # Replace :placeholders with '#{embedded options[:lookups]}' - prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}" } + prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.parser.escape options[#{key}].to_s}" } # Clear prefix parameters in case they have been cached @prefix_parameters = nil @@ -589,8 +603,8 @@ module ActiveResource def prefix(options={}) "#{prefix_call}" end RUBY_EVAL end - rescue - logger.error "Couldn't set prefix: #{$!}\n #{code}" if logger + rescue Exception => e + logger.error "Couldn't set prefix: #{e}\n #{code}" if logger raise end @@ -621,8 +635,10 @@ module ActiveResource # # => /posts/5/comments/1.xml?active=1 # def element_path(id, prefix_options = {}, query_options = nil) + check_prefix_options(prefix_options) + prefix_options, query_options = split_options(prefix_options) if query_options.nil? - "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}" + "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}.#{format.extension}#{query_string(query_options)}" end # Gets the new element path for REST resources. @@ -663,6 +679,7 @@ module ActiveResource # # => /posts/5/comments.xml?active=1 # def collection_path(prefix_options = {}, query_options = nil) + check_prefix_options(prefix_options) prefix_options, query_options = split_options(prefix_options) if query_options.nil? "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}" end @@ -678,7 +695,7 @@ module ActiveResource # Returns the new resource instance. # def build(attributes = {}) - attrs = connection.get("#{new_element_path}").merge(attributes) + attrs = self.format.decode(connection.get("#{new_element_path}").body).merge(attributes) self.new(attrs) end @@ -842,6 +859,14 @@ module ActiveResource end private + + def check_prefix_options(prefix_options) + p_options = HashWithIndifferentAccess.new(prefix_options) + prefix_parameters.each do |p| + raise(MissingPrefixParam, "#{p} prefix_option is missing") if p_options[p].blank? + end + end + # Find every resource def find_every(options) begin @@ -850,11 +875,11 @@ module ActiveResource instantiate_collection(get(from, options[:params])) when String path = "#{from}#{query_string(options[:params])}" - instantiate_collection(connection.get(path, headers) || []) + instantiate_collection(format.decode(connection.get(path, headers).body) || []) else prefix_options, query_options = split_options(options[:params]) path = collection_path(prefix_options, query_options) - instantiate_collection( (connection.get(path, headers) || []), prefix_options ) + instantiate_collection( (format.decode(connection.get(path, headers).body) || []), prefix_options ) end rescue ActiveResource::ResourceNotFound # Swallowing ResourceNotFound exceptions and return nil - as per @@ -870,7 +895,7 @@ module ActiveResource instantiate_record(get(from, options[:params])) when String path = "#{from}#{query_string(options[:params])}" - instantiate_record(connection.get(path, headers)) + instantiate_record(format.decode(connection.get(path, headers).body)) end end @@ -878,7 +903,7 @@ module ActiveResource def find_single(scope, options) prefix_options, query_options = split_options(options[:params]) path = element_path(scope, prefix_options, query_options) - instantiate_record(connection.get(path, headers), prefix_options) + instantiate_record(format.decode(connection.get(path, headers).body), prefix_options) end def instantiate_collection(collection, prefix_options = {}) @@ -886,7 +911,7 @@ module ActiveResource end def instantiate_record(record, prefix_options = {}) - new(record).tap do |resource| + new(record, true).tap do |resource| resource.prefix_options = prefix_options end end @@ -894,12 +919,12 @@ module ActiveResource # Accepts a URI and creates the site URI from that. def create_site_uri_from(site) - site.is_a?(URI) ? site.dup : uri_parser.parse(site) + site.is_a?(URI) ? site.dup : URI.parser.parse(site) end # Accepts a URI and creates the proxy URI from that. def create_proxy_uri_from(proxy) - proxy.is_a?(URI) ? proxy.dup : uri_parser.parse(proxy) + proxy.is_a?(URI) ? proxy.dup : URI.parser.parse(proxy) end # contains a set of the current prefix parameters. @@ -924,10 +949,6 @@ module ActiveResource [ prefix_options, query_options ] end - - def uri_parser - @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI - end end attr_accessor :attributes #:nodoc: @@ -959,9 +980,10 @@ module ActiveResource # # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling") # my_other_course.save - def initialize(attributes = {}) + def initialize(attributes = {}, persisted = false) @attributes = {}.with_indifferent_access @prefix_options = {} + @persisted = persisted load(attributes) end @@ -987,10 +1009,7 @@ module ActiveResource # not_ryan.hash # => {:not => "an ARes instance"} def clone # Clone all attributes except the pk and any nested ARes - cloned = attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.inject({}) do |attrs, (k, v)| - attrs[k] = v.clone - attrs - end + cloned = Hash[attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.map { |k, v| [k, v.clone] }] # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which # attempts to convert hashes into member objects and arrays into collections of objects. We want # the raw objects to be cloned so we bypass load by directly setting the attributes hash. @@ -1014,7 +1033,7 @@ module ActiveResource # is_new.new? # => false # def new? - id.nil? + !persisted? end alias :new_record? :new? @@ -1031,7 +1050,7 @@ module ActiveResource # not_persisted.persisted? # => true # def persisted? - !new? + @persisted end # Gets the <tt>\id</tt> attribute of the resource. @@ -1076,7 +1095,7 @@ module ActiveResource 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)] + # [(a = Person.find 1), (b = Person.find 2)] & [(c = Person.find 1), (d = Person.find 4)] # => [a] def hash id.hash end @@ -1318,8 +1337,9 @@ module ActiveResource end def load_attributes_from_response(response) - if response['Content-Length'] != "0" && response.body.strip.size > 0 + if !response['Content-Length'].blank? && response['Content-Length'] != "0" && !response.body.nil? && response.body.strip.size > 0 load(self.class.format.decode(response.body)) + @persisted = true end end @@ -1353,8 +1373,9 @@ module ActiveResource namespaces = module_names[0, module_names.size-1].map do |module_name| receiver = receiver.const_get(module_name) end - if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) } - return namespace.const_get(resource_name) + const_args = RUBY_VERSION < "1.9" ? [resource_name] : [resource_name, false] + if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(*const_args) } + return namespace.const_get(*const_args) else raise NameError end @@ -1370,8 +1391,9 @@ module ActiveResource self.class.const_get(resource_name) end rescue NameError - if self.class.const_defined?(resource_name) - resource = self.class.const_get(resource_name) + const_args = RUBY_VERSION < "1.9" ? [resource_name] : [resource_name, false] + if self.class.const_defined?(*const_args) + resource = self.class.const_get(*const_args) else resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base)) end diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index b7befe110d..480f2fbecb 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/benchmark' +require 'active_support/core_ext/uri' require 'net/https' require 'date' require 'time' @@ -31,21 +32,20 @@ module ActiveResource def initialize(site, format = ActiveResource::Formats::XmlFormat) raise ArgumentError, 'Missing site URI' unless site @user = @password = nil - @uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI self.site = site self.format = format end # Set URI for remote service. def site=(site) - @site = site.is_a?(URI) ? site : @uri_parser.parse(site) - @user = @uri_parser.unescape(@site.user) if @site.user - @password = @uri_parser.unescape(@site.password) if @site.password + @site = site.is_a?(URI) ? site : URI.parser.parse(site) + @user = URI.parser.unescape(@site.user) if @site.user + @password = URI.parser.unescape(@site.password) if @site.password end # Set the proxy for remote service. def proxy=(proxy) - @proxy = proxy.is_a?(URI) ? proxy : @uri_parser.parse(proxy) + @proxy = proxy.is_a?(URI) ? proxy : URI.parser.parse(proxy) end # Sets the user for remote service. @@ -76,7 +76,7 @@ module ActiveResource # Executes a GET request. # Used to get (find) resources. def get(path, headers = {}) - with_auth { format.decode(request(:get, path, build_request_headers(headers, :get, self.site.merge(path))).body) } + with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path))) } end # Executes a DELETE request (see HTTP protocol documentation if unfamiliar). diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb index dd3e35dfc7..9879f8cded 100644 --- a/activeresource/lib/active_resource/custom_methods.rb +++ b/activeresource/lib/active_resource/custom_methods.rb @@ -54,7 +54,7 @@ module ActiveResource # # Person.find(:all, :from => :active) def get(custom_method_name, options = {}) - connection.get(custom_method_collection_url(custom_method_name, options), headers) + format.decode(connection.get(custom_method_collection_url(custom_method_name, options), headers).body) end def post(custom_method_name, options = {}, body = '') @@ -85,7 +85,7 @@ module ActiveResource module InstanceMethods def get(method_name, options = {}) - connection.get(custom_method_element_url(method_name, options), self.class.headers) + self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body) end def post(method_name, options = {}, body = nil) diff --git a/activeresource/lib/active_resource/exceptions.rb b/activeresource/lib/active_resource/exceptions.rb index 0f4549fd73..6b953b28ad 100644 --- a/activeresource/lib/active_resource/exceptions.rb +++ b/activeresource/lib/active_resource/exceptions.rb @@ -36,6 +36,9 @@ module ActiveResource def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end end + # Raised when ... + class MissingPrefixParam < ArgumentError; end # :nodoc: + # 4xx Client Error class ClientError < ConnectionError; end # :nodoc: diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 75425c01c0..75649053d0 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -29,7 +29,8 @@ module ActiveResource # # In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>, # +path+ and <tt>request_headers</tt>. If no match is found an InvalidRequestError exception - # will be raised letting you know you need to create a new mock for that request. + # will be raised showing you what request it could not find a response for and also what requests and response + # pairs have been recorded so you can create a new mock for that request. # # ==== Example # def setup @@ -41,7 +42,7 @@ module ActiveResource # mock.delete "/people/1.xml", {}, nil, 200 # end # end - # + # # def test_get_matz # person = Person.find(1) # assert_equal "Matz", person.name @@ -59,10 +60,21 @@ module ActiveResource # end module_eval <<-EOE, __FILE__, __LINE__ + 1 def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {}) - @responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)] + request = Request.new(:#{method}, path, nil, request_headers) + response = Response.new(body || "", status, response_headers) + + delete_duplicate_responses(request) + + @responses << [request, response] end EOE end + + private + + def delete_duplicate_responses(request) + @responses.delete_if {|r| r[0] == request } + end end class << self @@ -77,13 +89,13 @@ module ActiveResource # mock.get "/people/1.xml", {}, @matz # end # end - # + # # def test_should_request_remote_service # person = Person.find(1) # Call the remote service - # + # # # This request object has the same HTTP method and path as declared by the mock # expected_request = ActiveResource::Request.new(:get, "/people/1.xml") - # + # # # Assert that the mock received, and responded to, the expected request from the model # assert ActiveResource::HttpMock.requests.include?(expected_request) # end @@ -97,18 +109,105 @@ module ActiveResource @@responses ||= [] end - # Accepts a block which declares a set of requests and responses for the HttpMock to respond to. See the main - # ActiveResource::HttpMock description for a more detailed explanation. - def respond_to(pairs = {}) #:yields: mock - reset! - responses.concat pairs.to_a + # Accepts a block which declares a set of requests and responses for the HttpMock to respond to in + # the following format: + # + # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {}) + # + # === Example + # + # @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + # ActiveResource::HttpMock.respond_to do |mock| + # mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml" + # mock.get "/people/1.xml", {}, @matz + # mock.put "/people/1.xml", {}, nil, 204 + # mock.delete "/people/1.xml", {}, nil, 200 + # end + # + # Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate + # these the following format: + # + # ActiveResource::Request.new(method, path, body, request_headers) + # ActiveResource::Response.new(body, status, response_headers) + # + # === Example + # + # Request.new(:#{method}, path, nil, request_headers) + # + # @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + # + # create_matz = ActiveResource::Request.new(:post, '/people.xml', @matz, {}) + # created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"}) + # get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil) + # ok_response = ActiveResource::Response.new("", 200, {}) + # + # pairs = {create_matz => created_response, get_matz => ok_response} + # + # ActiveResource::HttpMock.respond_to(pairs) + # + # Note, by default, every time you call +respond_to+, any previous request and response pairs stored + # in HttpMock will be deleted giving you a clean slate to work on. + # + # If you want to override this behaviour, pass in +false+ as the last argument to +respond_to+ + # + # === Example + # + # ActiveResource::HttpMock.respond_to do |mock| + # mock.send(:get, "/people/1", {}, "XML1") + # end + # ActiveResource::HttpMock.responses.length #=> 1 + # + # ActiveResource::HttpMock.respond_to(false) do |mock| + # mock.send(:get, "/people/2", {}, "XML2") + # end + # ActiveResource::HttpMock.responses.length #=> 2 + # + # This also works with passing in generated pairs of requests and responses, again, just pass in false + # as the last argument: + # + # === Example + # + # ActiveResource::HttpMock.respond_to do |mock| + # mock.send(:get, "/people/1", {}, "XML1") + # end + # ActiveResource::HttpMock.responses.length #=> 1 + # + # get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil) + # ok_response = ActiveResource::Response.new("", 200, {}) + # + # pairs = {get_matz => ok_response} + # + # ActiveResource::HttpMock.respond_to(pairs, false) + # ActiveResource::HttpMock.responses.length #=> 2 + # + # # If you add a response with an existing request, it will be replaced + # + # fail_response = ActiveResource::Response.new("", 404, {}) + # pairs = {get_matz => fail_response} + # + # ActiveResource::HttpMock.respond_to(pairs, false) + # ActiveResource::HttpMock.responses.length #=> 2 + # + def respond_to(*args) #:yields: mock + pairs = args.first || {} + reset! if args.last.class != FalseClass + if block_given? yield Responder.new(responses) else + delete_responses_to_replace pairs.to_a + responses.concat pairs.to_a Responder.new(responses) end end + def delete_responses_to_replace(new_responses) + new_responses.each{|nr| + request_to_remove = nr[0] + @@responses = responses.delete_if{|r| r[0] == request_to_remove} + } + end + # Deletes all logged requests and responses. def reset! requests.clear @@ -126,7 +225,7 @@ module ActiveResource # if response = self.class.responses.assoc(request) # response[1] # else - # raise InvalidRequestError.new("No response recorded for #{request}") + # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}") # end # end module_eval <<-EOE, __FILE__, __LINE__ + 1 @@ -136,7 +235,7 @@ module ActiveResource if response = self.class.responses.assoc(request) response[1] else - raise InvalidRequestError.new("No response recorded for \#{request}") + raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}") end end EOE @@ -146,6 +245,10 @@ module ActiveResource def initialize(site) #:nodoc: @site = site end + + def inspect_responses #:nodoc: + self.class.responses.map { |r| r[0].to_s }.inspect + end end class Request diff --git a/activeresource/lib/active_resource/schema.rb b/activeresource/lib/active_resource/schema.rb index 5758ac9502..3fd37a9bb6 100644 --- a/activeresource/lib/active_resource/schema.rb +++ b/activeresource/lib/active_resource/schema.rb @@ -4,7 +4,7 @@ module ActiveResource # :nodoc: class Schema # :nodoc: # attributes can be known to be one of these types. They are easy to # cast to/from. - KNOWN_ATTRIBUTE_TYPES = %w( string integer float ) + KNOWN_ATTRIBUTE_TYPES = %w( string text integer float decimal datetime timestamp time date binary boolean ) # An array of attribute definitions, representing the attributes that # have been defined. @@ -39,8 +39,6 @@ module ActiveResource # :nodoc: # The following are the attribute types supported by Active Resource # migrations. - # TODO: We should eventually support all of these: - # %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |attr_type| KNOWN_ATTRIBUTE_TYPES.each do |attr_type| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{attr_type.to_s}(*args) diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index 026d81e44a..a373e53f11 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -6,21 +6,21 @@ module ActiveResource end # 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. + # to determine whether the object in a valid state to be saved. See usage example in Validations. class Errors < ActiveModel::Errors # 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) } + humanized_attributes = Hash[@base.attributes.keys.map { |attr_name| [attr_name.humanize, attr_name] }] messages.each do |message| attr_message = humanized_attributes.keys.detect do |attr_name| if message[0, attr_name.size + 1] == "#{attr_name} " add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1] end end - + self[:base] << message if attr_message.nil? end end @@ -37,15 +37,15 @@ module ActiveResource from_array array, save_cache end end - + # Module to support validation and errors with Active Resource objects. The module overrides - # Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned - # in the web service response. The module also adds an +errors+ collection that mimics the interface + # Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned + # in the web service response. The module also adds an +errors+ collection that mimics the interface # of the errors provided by ActiveRecord::Errors. # # ==== Example # - # Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a + # Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a # <tt>validates_presence_of :first_name, :last_name</tt> declaration in the model: # # person = Person.new(:first_name => "Jim", :last_name => "") @@ -55,7 +55,7 @@ module ActiveResource # person.errors.count # => 1 # person.errors.full_messages # => ["Last name can't be empty"] # person.errors[:last_name] # => ["can't be empty"] - # person.last_name = "Halpert" + # person.last_name = "Halpert" # person.save # => true (and person is now saved to the remote service) # module Validations @@ -68,16 +68,8 @@ module ActiveResource # Validate a resource and save (POST) it to the remote web service. # If any local validations fail - the save (POST) will not be attempted. - def save_with_validation(options=nil) - perform_validation = case options - when Hash - options[:validate] != false - when NilClass - true - else - ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller - options - end + def save_with_validation(options={}) + perform_validation = options[:validate] != false # clear the remote validations so they don't interfere with the local # ones. Otherwise we get an endless loop and can never change the @@ -118,7 +110,7 @@ module ActiveResource # 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]) # my_person.valid? diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index 43c00e9cf1..82dcb5d575 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -1,10 +1,10 @@ module ActiveResource module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb index fcb770d612..195f93f2a6 100644 --- a/activeresource/test/abstract_unit.rb +++ b/activeresource/test/abstract_unit.rb @@ -18,3 +18,108 @@ begin require 'ruby-debug' rescue LoadError end + +def setup_response + @default_request_headers = { 'Content-Type' => 'application/xml' } + @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') + @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') + @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person') + @addy = { :id => 1, :street => '12345 Street', :country => 'Australia' }.to_xml(:root => 'address') + @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person") + @joe = { 'person' => { :id => 6, :name => 'Joe' }}.to_json + @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people') + @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people') + @addresses = [{ :id => 1, :street => '12345 Street', :country => 'Australia' }].to_xml(:root => 'addresses') + + # - deep nested resource - + # - Luis (Customer) + # - JK (Customer::Friend) + # - Mateo (Customer::Friend::Brother) + # - Edith (Customer::Friend::Brother::Child) + # - Martha (Customer::Friend::Brother::Child) + # - Felipe (Customer::Friend::Brother) + # - Bryan (Customer::Friend::Brother::Child) + # - Luke (Customer::Friend::Brother::Child) + # - Eduardo (Customer::Friend) + # - Sebas (Customer::Friend::Brother) + # - Andres (Customer::Friend::Brother::Child) + # - Jorge (Customer::Friend::Brother::Child) + # - Elsa (Customer::Friend::Brother) + # - Natacha (Customer::Friend::Brother::Child) + # - Milena (Customer::Friend::Brother) + # + @luis = {:id => 1, :name => 'Luis', + :friends => [{:name => 'JK', + :brothers => [{:name => 'Mateo', + :children => [{:name => 'Edith'},{:name => 'Martha'}]}, + {:name => 'Felipe', + :children => [{:name => 'Bryan'},{:name => 'Luke'}]}]}, + {:name => 'Eduardo', + :brothers => [{:name => 'Sebas', + :children => [{:name => 'Andres'},{:name => 'Jorge'}]}, + {:name => 'Elsa', + :children => [{:name => 'Natacha'}]}, + {:name => 'Milena', + :children => []}]}]}.to_xml(:root => 'customer') + # - resource with yaml array of strings; for ARs using serialize :bar, Array + @marty = <<-eof.strip + <?xml version=\"1.0\" encoding=\"UTF-8\"?> + <person> + <id type=\"integer\">5</id> + <name>Marty</name> + <colors type=\"yaml\">--- + - \"red\" + - \"green\" + - \"blue\" + </colors> + </person> + eof + + @startup_sound = { + :name => "Mac Startup Sound", :author => { :name => "Jim Reekes" } + }.to_xml(:root => 'sound') + + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", {}, @matz + mock.get "/people/2.xml", {}, @david + mock.get "/people/5.xml", {}, @marty + mock.get "/people/Greg.xml", {}, @greg + mock.get "/people/6.json", {}, @joe + mock.get "/people/4.xml", {'key' => 'value'}, nil, 404 + mock.put "/people/1.xml", {}, nil, 204 + mock.delete "/people/1.xml", {}, nil, 200 + mock.delete "/people/2.xml", {}, nil, 400 + mock.get "/people/99.xml", {}, nil, 404 + mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml' + mock.get "/people.xml", {}, @people + mock.get "/people/1/addresses.xml", {}, @addresses + mock.get "/people/1/addresses/1.xml", {}, @addy + mock.get "/people/1/addresses/2.xml", {}, nil, 404 + mock.get "/people/2/addresses.xml", {}, nil, 404 + mock.get "/people/2/addresses/1.xml", {}, nil, 404 + mock.get "/people/Greg/addresses/1.xml", {}, @addy + mock.put "/people/1/addresses/1.xml", {}, nil, 204 + mock.delete "/people/1/addresses/1.xml", {}, nil, 200 + mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5' + mock.get "/people/1/addresses/99.xml", {}, nil, 404 + mock.get "/people//addresses.xml", {}, nil, 404 + mock.get "/people//addresses/1.xml", {}, nil, 404 + mock.put "/people//addresses/1.xml", {}, nil, 404 + mock.delete "/people//addresses/1.xml", {}, nil, 404 + mock.post "/people//addresses.xml", {}, nil, 404 + mock.head "/people/1.xml", {}, nil, 200 + mock.head "/people/Greg.xml", {}, nil, 200 + mock.head "/people/99.xml", {}, nil, 404 + mock.head "/people/1/addresses/1.xml", {}, nil, 200 + mock.head "/people/1/addresses/2.xml", {}, nil, 404 + mock.head "/people/2/addresses/1.xml", {}, nil, 404 + mock.head "/people/Greg/addresses/1.xml", {}, nil, 200 + # customer + mock.get "/customers/1.xml", {}, @luis + # sound + mock.get "/sounds/1.xml", {}, @startup_sound + end + + Person.user = nil + Person.password = nil +end diff --git a/activeresource/test/cases/authorization_test.rb b/activeresource/test/cases/authorization_test.rb index 1a7c9ec8a4..a6797643e1 100644 --- a/activeresource/test/cases/authorization_test.rb +++ b/activeresource/test/cases/authorization_test.rb @@ -132,7 +132,7 @@ class AuthorizationTest < Test::Unit::TestCase end def test_get - david = @authenticated_conn.get("/people/2.xml") + david = decode(@authenticated_conn.get("/people/2.xml")) assert_equal "David", david["name"] end @@ -159,7 +159,7 @@ class AuthorizationTest < Test::Unit::TestCase def test_get_with_digest_auth_handles_initial_401_response_and_retries @authenticated_conn.auth_type = :digest response = @authenticated_conn.get("/people/2.xml") - assert_equal "David", response["name"] + assert_equal "David", decode(response)["name"] end def test_post_with_digest_auth_handles_initial_401_response_and_retries @@ -190,11 +190,11 @@ class AuthorizationTest < Test::Unit::TestCase def test_get_with_digest_auth_caches_nonce @authenticated_conn.auth_type = :digest response = @authenticated_conn.get("/people/2.xml") - assert_equal "David", response["name"] + assert_equal "David", decode(response)["name"] # There is no mock for this request with a non-cached nonce. response = @authenticated_conn.get("/people/1.xml") - assert_equal "Matz", response["name"] + assert_equal "Matz", decode(response)["name"] end def test_retry_on_401_only_happens_with_digest_auth @@ -241,4 +241,8 @@ class AuthorizationTest < Test::Unit::TestCase def response_digest_auth_header %Q(Digest realm="RailsTestApp", qop="auth", algorithm=MD5, nonce="#{@nonce}", opaque="ef6dfb078ba22298d366f99567814ffb") end + + def decode(response) + @authenticated_conn.format.decode(response.body) + end end diff --git a/activeresource/test/cases/base/custom_methods_test.rb b/activeresource/test/cases/base/custom_methods_test.rb index 2d81549a65..0fbf94bc0e 100644 --- a/activeresource/test/cases/base/custom_methods_test.rb +++ b/activeresource/test/cases/base/custom_methods_test.rb @@ -35,7 +35,7 @@ class CustomMethodsTest < Test::Unit::TestCase Person.user = nil Person.password = nil - end + end def teardown ActiveResource::HttpMock.reset! @@ -64,13 +64,13 @@ class CustomMethodsTest < Test::Unit::TestCase # Test GET against an element URL assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'} assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'} - + # Test PUT against an element URL assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body') - + # Test DELETE against an element URL assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate) - + # With nested resources assert_equal StreetAddress.find(1, :params => { :person_id => 1 }).get(:deep), { "id" => 1, "street" => '12345 Street', "zip" => "27519" } @@ -87,11 +87,11 @@ class CustomMethodsTest < Test::Unit::TestCase # Test POST against a nested collection URL addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1) - assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'), + assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'), 201, {'Location' => '/people/1/addresses/2.xml'}), addy.post(:link) - matz = Person.new(:id => 1, :name => 'Matz') + matz = Person.find(1) assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register) end diff --git a/activeresource/test/cases/base/schema_test.rb b/activeresource/test/cases/base/schema_test.rb index 136c827926..37f30e4353 100644 --- a/activeresource/test/cases/base/schema_test.rb +++ b/activeresource/test/cases/base/schema_test.rb @@ -8,58 +8,13 @@ require "fixtures/street_address" ######################################################################## class SchemaTest < ActiveModel::TestCase def setup - @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') - @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') - @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person') - @addy = { :id => 1, :street => '12345 Street', :country => 'Australia' }.to_xml(:root => 'address') - @default_request_headers = { 'Content-Type' => 'application/xml' } - @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person") - @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people') - @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people') - @addresses = [{ :id => 1, :street => '12345 Street', :country => 'Australia' }].to_xml(:root => 'addresses') - - ActiveResource::HttpMock.respond_to do |mock| - mock.get "/people/1.xml", {}, @matz - mock.get "/people/2.xml", {}, @david - mock.get "/people/Greg.xml", {}, @greg - mock.get "/people/4.xml", {'key' => 'value'}, nil, 404 - mock.get "/people/5.xml", {}, @rick - mock.put "/people/1.xml", {}, nil, 204 - mock.delete "/people/1.xml", {}, nil, 200 - mock.delete "/people/2.xml", {}, nil, 400 - mock.get "/people/99.xml", {}, nil, 404 - mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml' - mock.get "/people.xml", {}, @people - mock.get "/people/1/addresses.xml", {}, @addresses - mock.get "/people/1/addresses/1.xml", {}, @addy - mock.get "/people/1/addresses/2.xml", {}, nil, 404 - mock.get "/people/2/addresses/1.xml", {}, nil, 404 - mock.get "/people/Greg/addresses/1.xml", {}, @addy - mock.put "/people/1/addresses/1.xml", {}, nil, 204 - mock.delete "/people/1/addresses/1.xml", {}, nil, 200 - mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5' - mock.get "/people//addresses.xml", {}, nil, 404 - mock.get "/people//addresses/1.xml", {}, nil, 404 - mock.put "/people//addressaddresseses/1.xml", {}, nil, 404 - mock.delete "/people//addresses/1.xml", {}, nil, 404 - mock.post "/people//addresses.xml", {}, nil, 404 - mock.head "/people/1.xml", {}, nil, 200 - mock.head "/people/Greg.xml", {}, nil, 200 - mock.head "/people/99.xml", {}, nil, 404 - mock.head "/people/1/addresses/1.xml", {}, nil, 200 - mock.head "/people/1/addresses/2.xml", {}, nil, 404 - mock.head "/people/2/addresses/1.xml", {}, nil, 404 - mock.head "/people/Greg/addresses/1.xml", {}, nil, 200 - end - - Person.user = nil - Person.password = nil + setup_response # find me in abstract_unit end + def teardown Person.schema = nil # hack to stop test bleedthrough... end - ##################################################### # Passing in a schema directly and returning it #### @@ -78,14 +33,23 @@ class SchemaTest < ActiveModel::TestCase end test "schema should accept a simple hash" do - new_schema = {'age' => 'integer', 'name' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} + assert_nothing_raised { Person.schema = new_schema } assert_equal new_schema, Person.schema end test "schema should accept a hash with simple values" do - new_schema = {'age' => 'integer', 'name' => 'string', 'height' => 'float', 'mydatetime' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} assert_nothing_raised { Person.schema = new_schema } assert_equal new_schema, Person.schema @@ -109,7 +73,12 @@ class SchemaTest < ActiveModel::TestCase end test "schema should accept nil and remove the schema" do - new_schema = {'age' => 'integer', 'name' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} + assert_nothing_raised { Person.schema = new_schema } assert_equal new_schema, Person.schema # sanity check @@ -120,7 +89,12 @@ class SchemaTest < ActiveModel::TestCase test "schema should be with indifferent access" do - new_schema = {:age => :integer, 'name' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} + new_schema_syms = new_schema.keys assert_nothing_raised { Person.schema = new_schema } @@ -156,7 +130,12 @@ class SchemaTest < ActiveModel::TestCase test "defining a schema should return it when asked" do assert Person.schema.blank?, "should have a blank class schema" - new_schema = {'name' => 'string', 'age' => 'integer', 'height' => 'float', 'weight' => 'float'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} + assert_nothing_raised { Person.schema = new_schema assert_equal new_schema, Person.schema, "should have saved the schema on the class" @@ -167,7 +146,11 @@ class SchemaTest < ActiveModel::TestCase test "defining a schema, then fetching a model should still match the defined schema" do # sanity checks assert Person.schema.blank?, "should have a blank class schema" - new_schema = {'name' => 'string', 'age' => 'integer', 'height' => 'float', 'weight' => 'float'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} matz = Person.find(1) assert !matz.schema.blank?, "should have some sort of schema on an instance variable" @@ -368,12 +351,16 @@ class SchemaTest < ActiveModel::TestCase end test "setting schema should set known attributes on class and instance" do - new_schema = {'age' => 'integer', 'name' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} assert_nothing_raised { Person.schema = new_schema } - assert_equal new_schema.keys, Person.known_attributes - assert_equal new_schema.keys, Person.new.known_attributes + assert_equal new_schema.keys.sort, Person.known_attributes.sort + assert_equal new_schema.keys.sort, Person.new.known_attributes.sort end test "known attributes on a fetched resource should return all the attributes of the instance" do @@ -399,9 +386,13 @@ class SchemaTest < ActiveModel::TestCase assert_not_equal matz.known_attributes, rick.known_attributes, "should have had different known attributes too" end - test "setting schema then fetching should add schema attributes to the intance attributes" do + test "setting schema then fetching should add schema attributes to the instance attributes" do # an attribute in common with fetched instance and one that isn't - new_schema = {'name' => 'string', 'my_strange_attribute' => 'string'} + new_schema = {'age' => 'integer', 'name' => 'string', + 'height' => 'float', 'bio' => 'text', + 'weight' => 'decimal', 'photo' => 'binary', + 'alive' => 'boolean', 'created_at' => 'timestamp', + 'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'} assert_nothing_raised { Person.schema = new_schema } diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index 91b375681b..48dacbdf67 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -5,6 +5,8 @@ require "fixtures/street_address" require "fixtures/sound" require "fixtures/beast" require "fixtures/proxy" +require "fixtures/address" +require "fixtures/subscription_plan" require 'active_support/json' require 'active_support/ordered_hash' require 'active_support/core_ext/hash/conversions' @@ -12,101 +14,8 @@ require 'mocha' class BaseTest < Test::Unit::TestCase def setup - @default_request_headers = { 'Content-Type' => 'application/xml' } - @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') - @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') - @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person') - @addy = { :id => 1, :street => '12345 Street', :country => 'Australia' }.to_xml(:root => 'address') - @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person") - @joe = { 'person' => { :id => 6, :name => 'Joe' }}.to_json - @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people') - @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people') - @addresses = [{ :id => 1, :street => '12345 Street', :country => 'Australia' }].to_xml(:root => 'addresses') - - # - deep nested resource - - # - Luis (Customer) - # - JK (Customer::Friend) - # - Mateo (Customer::Friend::Brother) - # - Edith (Customer::Friend::Brother::Child) - # - Martha (Customer::Friend::Brother::Child) - # - Felipe (Customer::Friend::Brother) - # - Bryan (Customer::Friend::Brother::Child) - # - Luke (Customer::Friend::Brother::Child) - # - Eduardo (Customer::Friend) - # - Sebas (Customer::Friend::Brother) - # - Andres (Customer::Friend::Brother::Child) - # - Jorge (Customer::Friend::Brother::Child) - # - Elsa (Customer::Friend::Brother) - # - Natacha (Customer::Friend::Brother::Child) - # - Milena (Customer::Friend::Brother) - # - @luis = {:id => 1, :name => 'Luis', - :friends => [{:name => 'JK', - :brothers => [{:name => 'Mateo', - :children => [{:name => 'Edith'},{:name => 'Martha'}]}, - {:name => 'Felipe', - :children => [{:name => 'Bryan'},{:name => 'Luke'}]}]}, - {:name => 'Eduardo', - :brothers => [{:name => 'Sebas', - :children => [{:name => 'Andres'},{:name => 'Jorge'}]}, - {:name => 'Elsa', - :children => [{:name => 'Natacha'}]}, - {:name => 'Milena', - :children => []}]}]}.to_xml(:root => 'customer') - # - resource with yaml array of strings; for ARs using serialize :bar, Array - @marty = <<-eof.strip - <?xml version=\"1.0\" encoding=\"UTF-8\"?> - <person> - <id type=\"integer\">5</id> - <name>Marty</name> - <colors type=\"yaml\">--- - - \"red\" - - \"green\" - - \"blue\" - </colors> - </person> - eof - - ActiveResource::HttpMock.respond_to do |mock| - mock.get "/people/1.xml", {}, @matz - mock.get "/people/2.xml", {}, @david - mock.get "/people/6.json", {}, @joe - mock.get "/people/5.xml", {}, @marty - mock.get "/people/Greg.xml", {}, @greg - mock.get "/people/4.xml", {'key' => 'value'}, nil, 404 - mock.put "/people/1.xml", {}, nil, 204 - mock.delete "/people/1.xml", {}, nil, 200 - mock.delete "/people/2.xml", {}, nil, 400 - mock.get "/people/99.xml", {}, nil, 404 - mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml' - mock.get "/people.xml", {}, @people - mock.get "/people/1/addresses.xml", {}, @addresses - mock.get "/people/1/addresses/1.xml", {}, @addy - mock.get "/people/1/addresses/2.xml", {}, nil, 404 - mock.get "/people/2/addresses/1.xml", {}, nil, 404 - mock.get "/people/Greg/addresses/1.xml", {}, @addy - mock.put "/people/1/addresses/1.xml", {}, nil, 204 - mock.delete "/people/1/addresses/1.xml", {}, nil, 200 - mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5' - mock.get "/people//addresses.xml", {}, nil, 404 - mock.get "/people//addresses/1.xml", {}, nil, 404 - mock.put "/people//addresses/1.xml", {}, nil, 404 - mock.delete "/people//addresses/1.xml", {}, nil, 404 - mock.post "/people//addresses.xml", {}, nil, 404 - mock.head "/people/1.xml", {}, nil, 200 - mock.head "/people/Greg.xml", {}, nil, 200 - mock.head "/people/99.xml", {}, nil, 404 - mock.head "/people/1/addresses/1.xml", {}, nil, 200 - mock.head "/people/1/addresses/2.xml", {}, nil, 404 - mock.head "/people/2/addresses/1.xml", {}, nil, 404 - mock.head "/people/Greg/addresses/1.xml", {}, nil, 200 - # customer - mock.get "/customers/1.xml", {}, @luis - end - + setup_response # find me in abstract_unit @original_person_site = Person.site - Person.user = nil - Person.password = nil end def teardown @@ -554,9 +463,9 @@ class BaseTest < Test::Unit::TestCase assert Person.collection_path(:gender => 'male', :student => true).include?('gender=male') assert Person.collection_path(:gender => 'male', :student => true).include?('student=true') - assert_equal '/people.xml?name[]=bob&name[]=your+uncle%2Bme&name[]=&name[]=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) + assert_equal '/people.xml?name%5B%5D=bob&name%5B%5D=your+uncle%2Bme&name%5B%5D=&name%5B%5D=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) - assert_equal '/people.xml?struct[a][]=2&struct[a][]=1&struct[b]=fred', Person.collection_path(:struct => ActiveSupport::OrderedHash[:a, [2,1], 'b', 'fred']) + assert_equal '/people.xml?struct%5Ba%5D%5B%5D=2&struct%5Ba%5D%5B%5D=1&struct%5Bb%5D=fred', Person.collection_path(:struct => ActiveSupport::OrderedHash[:a, [2,1], 'b', 'fred']) end def test_custom_element_path @@ -566,6 +475,12 @@ class BaseTest < Test::Unit::TestCase assert_equal '/people/ann%20mary/addresses/ann%20mary.xml', StreetAddress.element_path(:'ann mary', 'person_id' => 'ann mary') end + def test_custom_element_path_without_required_prefix_param + assert_raise ActiveResource::MissingPrefixParam do + StreetAddress.element_path(1) + end + end + def test_module_element_path assert_equal '/sounds/1.xml', Asset::Sound.element_path(1) end @@ -597,13 +512,19 @@ class BaseTest < Test::Unit::TestCase assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, :person_id => 1, :type => 'work') assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, 'person_id' => 1, :type => 'work') assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, :type => 'work', :person_id => 1) - assert_equal '/people/1/addresses/1.xml?type[]=work&type[]=play+time', StreetAddress.element_path(1, :person_id => 1, :type => ['work', 'play time']) + assert_equal '/people/1/addresses/1.xml?type%5B%5D=work&type%5B%5D=play+time', StreetAddress.element_path(1, :person_id => 1, :type => ['work', 'play time']) end def test_custom_element_path_with_prefix_and_parameters assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, {:person_id => 1}, {:type => 'work'}) end + def test_custom_collection_path_without_required_prefix_param + assert_raise ActiveResource::MissingPrefixParam do + StreetAddress.collection_path + end + end + def test_custom_collection_path assert_equal '/people/1/addresses.xml', StreetAddress.collection_path(:person_id => 1) assert_equal '/people/1/addresses.xml', StreetAddress.collection_path('person_id' => 1) @@ -651,6 +572,8 @@ class BaseTest < Test::Unit::TestCase assert_equal Set.new([:the_param1]), person_class.prefix_parameters person_class.prefix = "the_prefix/:the_param2" assert_equal Set.new([:the_param2]), person_class.prefix_parameters + person_class.prefix = "the_prefix/:the_param1/other_prefix/:the_param2" + assert_equal Set.new([:the_param2, :the_param1]), person_class.prefix_parameters end end @@ -713,6 +636,14 @@ class BaseTest < Test::Unit::TestCase assert_nil p.__send__(:id_from_response, resp) end + def test_load_attributes_from_response + p = Person.new + resp = ActiveResource::Response.new(nil) + resp['Content-Length'] = "100" + assert_nil p.__send__(:load_attributes_from_response, resp) + end + + def test_create_with_custom_prefix matzs_house = StreetAddress.new(:person_id => 1) matzs_house.save @@ -945,7 +876,7 @@ class BaseTest < Test::Unit::TestCase end ######################################################################## - # Tests the more miscelaneous helper methods + # Tests the more miscellaneous helper methods ######################################################################## def test_exists # Class method. @@ -1042,6 +973,12 @@ class BaseTest < Test::Unit::TestCase Person.element_name = old_elem_name end + def test_to_xml_with_private_method_name_as_attribute + assert_nothing_raised(ArgumentError) { + Customer.new(:test => true).to_xml + } + end + def test_to_json Person.include_root_in_json = true Person.format = :json @@ -1113,4 +1050,56 @@ class BaseTest < Test::Unit::TestCase end end end + + def test_with_custom_formatter + @addresses = [{:id => "1", :street => "1 Infinite Loop", :city => "Cupertino", :state => "CA"}].to_xml(:root => 'addresses') + + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/addresses.xml", {}, @addresses, 200 + end + + # late bind the site + AddressResource.site = "http://localhost" + addresses = AddressResource.find(:all) + + assert_equal "Cupertino, CA", addresses.first.city_state + end + + def test_create_with_custom_primary_key + silver_plan = {:code => "silver", :price => 5.00}.to_xml(:root => "plan") + + ActiveResource::HttpMock.respond_to do |mock| + mock.post "/plans.xml", {}, silver_plan, 201, 'Location' => '/plans/silver.xml' + end + + plan = SubscriptionPlan.new(:code => "silver", :price => 5.00) + assert plan.new? + + plan.save! + assert !plan.new? + end + + def test_update_with_custom_primary_key + silver_plan = {:code => "silver", :price => 5.00}.to_xml(:root => "plan") + silver_plan_updated = {:code => "silver", :price => 10.00}.to_xml(:root => "plan") + + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/plans/silver.xml", {}, silver_plan + mock.put "/plans/silver.xml", {}, silver_plan_updated, 201, 'Location' => '/plans/silver.xml' + end + + plan = SubscriptionPlan.find("silver") + assert !plan.new? + assert_equal 5.00, plan.price + + # update price + plan.price = 10.00 + plan.save! + assert_equal 10.00, plan.price + end + + def test_namespacing + sound = Asset::Sound.find(1) + assert_equal "Asset::Sound::Author", sound.author.class.to_s + end end diff --git a/activeresource/test/cases/finder_test.rb b/activeresource/test/cases/finder_test.rb index 1f52868966..ebb783996d 100644 --- a/activeresource/test/cases/finder_test.rb +++ b/activeresource/test/cases/finder_test.rb @@ -8,101 +8,7 @@ require 'active_support/core_ext/hash/conversions' class FinderTest < Test::Unit::TestCase def setup - # TODO: refactor/DRY this setup - it's a copy of the BaseTest setup. - # We can probably put this into abstract_unit - @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') - @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') - @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person') - @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address') - @default_request_headers = { 'Content-Type' => 'application/xml' } - @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person") - @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people') - @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people') - @addresses = [{ :id => 1, :street => '12345 Street' }].to_xml(:root => 'addresses') - - # - deep nested resource - - # - Luis (Customer) - # - JK (Customer::Friend) - # - Mateo (Customer::Friend::Brother) - # - Edith (Customer::Friend::Brother::Child) - # - Martha (Customer::Friend::Brother::Child) - # - Felipe (Customer::Friend::Brother) - # - Bryan (Customer::Friend::Brother::Child) - # - Luke (Customer::Friend::Brother::Child) - # - Eduardo (Customer::Friend) - # - Sebas (Customer::Friend::Brother) - # - Andres (Customer::Friend::Brother::Child) - # - Jorge (Customer::Friend::Brother::Child) - # - Elsa (Customer::Friend::Brother) - # - Natacha (Customer::Friend::Brother::Child) - # - Milena (Customer::Friend::Brother) - # - @luis = {:id => 1, :name => 'Luis', - :friends => [{:name => 'JK', - :brothers => [{:name => 'Mateo', - :children => [{:name => 'Edith'},{:name => 'Martha'}]}, - {:name => 'Felipe', - :children => [{:name => 'Bryan'},{:name => 'Luke'}]}]}, - {:name => 'Eduardo', - :brothers => [{:name => 'Sebas', - :children => [{:name => 'Andres'},{:name => 'Jorge'}]}, - {:name => 'Elsa', - :children => [{:name => 'Natacha'}]}, - {:name => 'Milena', - :children => []}]}]}.to_xml(:root => 'customer') - # - resource with yaml array of strings; for ARs using serialize :bar, Array - @marty = <<-eof.strip - <?xml version=\"1.0\" encoding=\"UTF-8\"?> - <person> - <id type=\"integer\">5</id> - <name>Marty</name> - <colors type=\"yaml\">--- - - \"red\" - - \"green\" - - \"blue\" - </colors> - </person> - eof - - ActiveResource::HttpMock.respond_to do |mock| - mock.get "/people/1.xml", {}, @matz - mock.get "/people/2.xml", {}, @david - mock.get "/people/5.xml", {}, @marty - mock.get "/people/Greg.xml", {}, @greg - mock.get "/people/4.xml", {'key' => 'value'}, nil, 404 - mock.put "/people/1.xml", {}, nil, 204 - mock.delete "/people/1.xml", {}, nil, 200 - mock.delete "/people/2.xml", {}, nil, 400 - mock.get "/people/99.xml", {}, nil, 404 - mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml' - mock.get "/people.xml", {}, @people - mock.get "/people/1/addresses.xml", {}, @addresses - mock.get "/people/1/addresses/1.xml", {}, @addy - mock.get "/people/1/addresses/2.xml", {}, nil, 404 - mock.get "/people/2/addresses.xml", {}, nil, 404 - mock.get "/people/2/addresses/1.xml", {}, nil, 404 - mock.get "/people/Greg/addresses/1.xml", {}, @addy - mock.put "/people/1/addresses/1.xml", {}, nil, 204 - mock.delete "/people/1/addresses/1.xml", {}, nil, 200 - mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5' - mock.get "/people//addresses.xml", {}, nil, 404 - mock.get "/people//addresses/1.xml", {}, nil, 404 - mock.put "/people//addresses/1.xml", {}, nil, 404 - mock.delete "/people//addresses/1.xml", {}, nil, 404 - mock.post "/people//addresses.xml", {}, nil, 404 - mock.head "/people/1.xml", {}, nil, 200 - mock.head "/people/Greg.xml", {}, nil, 200 - mock.head "/people/99.xml", {}, nil, 404 - mock.head "/people/1/addresses/1.xml", {}, nil, 200 - mock.head "/people/1/addresses/2.xml", {}, nil, 404 - mock.head "/people/2/addresses/1.xml", {}, nil, 404 - mock.head "/people/Greg/addresses/1.xml", {}, nil, 200 - # customer - mock.get "/customers/1.xml", {}, @luis - end - - Person.user = nil - Person.password = nil + setup_response # find me in abstract_unit end def test_find_by_id @@ -178,7 +84,7 @@ class FinderTest < Test::Unit::TestCase def test_find_by_id_not_found assert_raise(ActiveResource::ResourceNotFound) { Person.find(99) } - assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1) } + assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(99, :params => {:person_id => 1}) } end def test_find_all_sub_objects diff --git a/activeresource/test/cases/format_test.rb b/activeresource/test/cases/format_test.rb index c3733e13d8..fc1a7b8c6f 100644 --- a/activeresource/test/cases/format_test.rb +++ b/activeresource/test/cases/format_test.rb @@ -33,7 +33,7 @@ class FormatTest < Test::Unit::TestCase ActiveResource::HttpMock.respond_to.get "/people.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@programmers) remote_programmers = Person.find(:all) assert_equal 2, remote_programmers.size - assert remote_programmers.select { |p| p.name == 'David' } + assert remote_programmers.find { |p| p.name == 'David' } end end end diff --git a/activeresource/test/cases/http_mock_test.rb b/activeresource/test/cases/http_mock_test.rb index 5e032d03f1..43cf5f5ef0 100644 --- a/activeresource/test/cases/http_mock_test.rb +++ b/activeresource/test/cases/http_mock_test.rb @@ -59,6 +59,136 @@ class HttpMockTest < ActiveSupport::TestCase assert_equal "XML", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body assert_equal "Json", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body end + + test "raises InvalidRequestError if no response found for the #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML") + end + + assert_raise(::ActiveResource::InvalidRequestError) do + request(method, "/people/1", FORMAT_HEADER[method] => "application/json") + end + end + + end + + test "allows you to send in pairs directly to the respond_to method" do + matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + + create_matz = ActiveResource::Request.new(:post, '/people.xml', matz, {}) + created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"}) + get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil) + ok_response = ActiveResource::Response.new(matz, 200, {}) + + pairs = {create_matz => created_response, get_matz => ok_response} + + ActiveResource::HttpMock.respond_to(pairs) + assert_equal 2, ActiveResource::HttpMock.responses.length + assert_equal "", ActiveResource::HttpMock.responses.assoc(create_matz)[1].body + assert_equal matz, ActiveResource::HttpMock.responses.assoc(get_matz)[1].body + end + + test "resets all mocked responses on each call to respond_to with a block by default" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/2", {}, "XML2") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + end + + test "resets all mocked responses on each call to respond_to by passing pairs by default" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil) + ok_response = ActiveResource::Response.new(matz, 200, {}) + ActiveResource::HttpMock.respond_to({get_matz => ok_response}) + + assert_equal 1, ActiveResource::HttpMock.responses.length + end + + test "allows you to add new responses to the existing responses by calling a block" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + ActiveResource::HttpMock.respond_to(false) do |mock| + mock.send(:get, "/people/2", {}, "XML2") + end + assert_equal 2, ActiveResource::HttpMock.responses.length + end + + test "allows you to add new responses to the existing responses by passing pairs" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil) + ok_response = ActiveResource::Response.new(matz, 200, {}) + ActiveResource::HttpMock.respond_to({get_matz => ok_response}, false) + + assert_equal 2, ActiveResource::HttpMock.responses.length + end + + test "allows you to replace the existing reponse with the same request by calling a block" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + ActiveResource::HttpMock.respond_to(false) do |mock| + mock.send(:get, "/people/1", {}, "XML2") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + end + + test "allows you to replace the existing reponse with the same request by passing pairs" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person") + get_matz = ActiveResource::Request.new(:get, '/people/1', nil) + ok_response = ActiveResource::Response.new(matz, 200, {}) + + ActiveResource::HttpMock.respond_to({get_matz => ok_response}, false) + assert_equal 1, ActiveResource::HttpMock.responses.length + end + + test "do not replace the response with the same path but different method by calling a block" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + ActiveResource::HttpMock.respond_to(false) do |mock| + mock.send(:put, "/people/1", {}, "XML2") + end + assert_equal 2, ActiveResource::HttpMock.responses.length + end + + test "do not replace the response with the same path but different method by passing pairs" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(:get, "/people/1", {}, "XML1") + end + assert_equal 1, ActiveResource::HttpMock.responses.length + + put_matz = ActiveResource::Request.new(:put, '/people/1', nil) + ok_response = ActiveResource::Response.new("", 200, {}) + + ActiveResource::HttpMock.respond_to({put_matz => ok_response}, false) + assert_equal 2, ActiveResource::HttpMock.responses.length end def request(method, path, headers = {}, body = nil) diff --git a/activeresource/test/cases/validations_test.rb b/activeresource/test/cases/validations_test.rb index 82546424f2..3b1caecb04 100644 --- a/activeresource/test/cases/validations_test.rb +++ b/activeresource/test/cases/validations_test.rb @@ -3,7 +3,7 @@ require 'fixtures/project' require 'active_support/core_ext/hash/conversions' # The validations are tested thoroughly under ActiveModel::Validations -# This test case simply makes sur that they are all accessible by +# This test case simply makes sure that they are all accessible by # Active Resource objects. class ValidationsTest < ActiveModel::TestCase VALID_PROJECT_HASH = { :name => "My Project", :description => "A project" } @@ -24,7 +24,7 @@ class ValidationsTest < ActiveModel::TestCase assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}" end - + def test_fails_save! p = new_project(:name => nil) assert_raise(ActiveResource::ResourceInvalid) { p.save! } @@ -36,14 +36,6 @@ class ValidationsTest < ActiveModel::TestCase assert p.save(:validate => false) end - def test_deprecated_save_without_validation - p = new_project(:name => nil) - assert !p.save - assert_deprecated do - assert p.save(false) - end - end - def test_validate_callback # we have a callback ensuring the description is longer than three letters p = new_project(:description => 'a') @@ -56,6 +48,12 @@ class ValidationsTest < ActiveModel::TestCase assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}" end + def test_client_side_validation_maximum + project = Project.new(:description => '123456789012345') + assert ! project.valid? + assert_equal ['is too long (maximum is 10 characters)'], project.errors[:description] + end + protected # quickie helper to create a new project with all the required diff --git a/activeresource/test/connection_test.rb b/activeresource/test/connection_test.rb index a2744d7531..6e79845aa0 100644 --- a/activeresource/test/connection_test.rb +++ b/activeresource/test/connection_test.rb @@ -44,7 +44,7 @@ class ConnectionTest < Test::Unit::TestCase # 401 is an unauthorized request assert_response_raises ActiveResource::UnauthorizedAccess, 401 - # 403 is a forbidden requst (and authorizing will not help) + # 403 is a forbidden request (and authorizing will not help) assert_response_raises ActiveResource::ForbiddenAccess, 403 # 404 is a missing resource. @@ -120,7 +120,7 @@ class ConnectionTest < Test::Unit::TestCase end def test_get - matz = @conn.get("/people/1.xml") + matz = decode(@conn.get("/people/1.xml")) assert_equal "Matz", matz["name"] end @@ -131,23 +131,23 @@ class ConnectionTest < Test::Unit::TestCase end def test_get_with_header - david = @conn.get("/people/2.xml", @header) + david = decode(@conn.get("/people/2.xml", @header)) assert_equal "David", david["name"] end def test_get_collection - people = @conn.get("/people.xml") + people = decode(@conn.get("/people.xml")) assert_equal "Matz", people[0]["name"] assert_equal "David", people[1]["name"] end - + def test_get_collection_single - people = @conn.get("/people_single_elements.xml") + people = decode(@conn.get("/people_single_elements.xml")) assert_equal "Matz", people[0]["name"] end - + def test_get_collection_empty - people = @conn.get("/people_empty_elements.xml") + people = decode(@conn.get("/people_empty_elements.xml")) assert_equal [], people end @@ -250,4 +250,8 @@ class ConnectionTest < Test::Unit::TestCase def handle_response(response) @conn.__send__(:handle_response, response) end + + def decode(response) + @conn.format.decode(response.body) + end end diff --git a/activeresource/test/fixtures/address.rb b/activeresource/test/fixtures/address.rb new file mode 100644 index 0000000000..7a73ecb52a --- /dev/null +++ b/activeresource/test/fixtures/address.rb @@ -0,0 +1,19 @@ +# turns everything into the same object +class AddressXMLFormatter + include ActiveResource::Formats::XmlFormat + + def decode(xml) + data = ActiveResource::Formats::XmlFormat.decode(xml) + # process address fields + data.each do |address| + address['city_state'] = "#{address['city']}, #{address['state']}" + end + data + end + +end + +class AddressResource < ActiveResource::Base + self.element_name = "address" + self.format = AddressXMLFormatter.new +end
\ No newline at end of file diff --git a/activeresource/test/fixtures/project.rb b/activeresource/test/fixtures/project.rb index e15fa6f620..53de666601 100644 --- a/activeresource/test/fixtures/project.rb +++ b/activeresource/test/fixtures/project.rb @@ -1,25 +1,18 @@ # used to test validations class Project < ActiveResource::Base self.site = "http://37s.sunrise.i:3000" + schema do + string :email + string :name + end - validates_presence_of :name + validates :name, :presence => true + validates :description, :presence => false, :length => {:maximum => 10} validate :description_greater_than_three_letters # to test the validate *callback* works def description_greater_than_three_letters errors.add :description, 'must be greater than three letters long' if description.length < 3 unless description.blank? end - - - # stop-gap accessor to default this attribute to nil - # Otherwise the validations fail saying that the method does not exist. - # In future, method_missing will be updated to not explode on a known - # attribute. - def name - attributes['name'] || nil - end - def description - attributes['description'] || nil - end end diff --git a/activeresource/test/fixtures/sound.rb b/activeresource/test/fixtures/sound.rb index 5c0ef5d55c..d9d2b99fcd 100644 --- a/activeresource/test/fixtures/sound.rb +++ b/activeresource/test/fixtures/sound.rb @@ -1,5 +1,9 @@ -module Asset +module Asset class Sound < ActiveResource::Base self.site = "http://37s.sunrise.i:3000" end end + +# to test namespacing in a module +class Author +end
\ No newline at end of file diff --git a/activeresource/test/fixtures/subscription_plan.rb b/activeresource/test/fixtures/subscription_plan.rb new file mode 100644 index 0000000000..e3c2dd9a74 --- /dev/null +++ b/activeresource/test/fixtures/subscription_plan.rb @@ -0,0 +1,5 @@ +class SubscriptionPlan < ActiveResource::Base + self.site = "http://37s.sunrise.i:3000" + self.element_name = 'plan' + self.primary_key = :code +end |