diff --git a/lib/active_resource.rb b/lib/active_resource.rb index 1d970436f4..204d5acd8f 100644 --- a/lib/active_resource.rb +++ b/lib/active_resource.rb @@ -36,5 +36,6 @@ module ActiveResource autoload :HttpMock autoload :Observing autoload :Schema + autoload :Singleton autoload :Validations end diff --git a/lib/active_resource/singleton.rb b/lib/active_resource/singleton.rb new file mode 100644 index 0000000000..9718c718a9 --- /dev/null +++ b/lib/active_resource/singleton.rb @@ -0,0 +1,114 @@ +module ActiveResource + module Singleton + extend ActiveSupport::Concern + + module ClassMethods + attr_writer :singleton_name + + def singleton_name + @singleton_name ||= model_name.element + end + + # Gets the singleton path for the object. If the +query_options+ parameter is omitted, Rails + # will split from the \prefix options. + # + # ==== Options + # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., :account_id => 19 + # would yield a URL like /accounts/19/purchases.json). + # +query_options+ - A \hash to add items to the query string for the request. + # + # ==== Examples + # Weather.singleton_path + # # => /weather.json + # + # class Inventory < ActiveResource::Base + # self.site = "https://37s.sunrise.com" + # self.prefix = "/products/:product_id/" + # end + # + # Inventory.singleton_path(:product_id => 5) + # # => /products/5/inventory.json + # + # Inventory.singleton_path({:product_id => 5}, {:sold => true}) + # # => /products/5/inventory.json?sold=true + # + def singleton_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)}#{singleton_name}.#{format.extension}#{query_string(query_options)}" + end + + # Core method for finding singleton resources. + # + # ==== Arguments + # Takes a single argument of options + # + # ==== Options + # * :params - Sets the query and \prefix (nested URL) parameters. + # + # ==== Examples + # Weather.find + # # => GET /weather.json + # + # Weather.find(:params => {:degrees => 'fahrenheit'}) + # # => GET /weather.json?degrees=fahrenheit + # + # == Failure or missing data + # A failure to find the requested object raises a ResourceNotFound + # exception. + # + # Inventory.find + # # => raises ResourceNotFound + def find(options={}) + find_singleton(options) + end + + private + # Find singleton resource + def find_singleton(options) + prefix_options, query_options = split_options(options[:params]) + + path = singleton_path(prefix_options, query_options) + resp = self.format.decode(self.connection.get(path, self.headers).body) + instantiate_record(resp, {}) + end + + end + # Deletes the resource from the remove service. + # + # ==== Examples + # weather = Weather.find + # weather.destroy + # Weather.find # 404 (Resource Not Found) + def destroy + connection.delete(singleton_path, self.class.headers) + end + + + protected + + # Update the resource on the remote service + def update + connection.put(singleton_path(prefix_options), encode, self.class.headers).tap do |response| + load_attributes_from_response(response) + end + end + + # Create (i.e. \save to the remote service) the \new resource. + def create + connection.post(singleton_path, encode, self.class.headers).tap do |response| + self.id = id_from_response(response) + load_attributes_from_response(response) + end + end + + private + + def singleton_path(options = nil) + self.class.singleton_path(options || prefix_options) + end + + end + +end diff --git a/test/fixtures/inventory.rb b/test/fixtures/inventory.rb new file mode 100644 index 0000000000..4df01f419d --- /dev/null +++ b/test/fixtures/inventory.rb @@ -0,0 +1,13 @@ +class Inventory < ActiveResource::Base + include ActiveResource::Singleton + self.site = 'http://37s.sunrise.i:3000' + self.prefix = '/products/:product_id/' + + schema do + integer :total + integer :used + + string :status + end +end + diff --git a/test/fixtures/weather.rb b/test/fixtures/weather.rb new file mode 100644 index 0000000000..d0fc1c34e3 --- /dev/null +++ b/test/fixtures/weather.rb @@ -0,0 +1,19 @@ +class Weather < ActiveResource::Base + include ActiveResource::Singleton + self.site = 'http://37s.sunrise.i:3000' + + schema do + string :status + string :temperature + end +end + +class WeatherDashboard < ActiveResource::Base + include ActiveResource::Singleton + self.site = 'http://37s.sunrise.i:3000' + self.singleton_name = 'dashboard' + + schema do + string :status + end +end diff --git a/test/singleton_test.rb b/test/singleton_test.rb new file mode 100644 index 0000000000..bfad3510ca --- /dev/null +++ b/test/singleton_test.rb @@ -0,0 +1,136 @@ +require 'abstract_unit' +require 'fixtures/weather' +require 'fixtures/inventory' + +class SingletonTest < ActiveSupport::TestCase + def setup_weather + weather = { :status => 'Sunny', :temperature => 67 } + ActiveResource::HttpMock.respond_to do |mock| + mock.get '/weather.json', {}, weather.to_json + mock.get '/weather.json?degrees=fahrenheit', {}, weather.merge(:temperature => 100).to_json + mock.post '/weather.json', {}, weather.to_json, 201, 'Location' => '/weather.json' + mock.delete '/weather.json', {}, nil + mock.put '/weather.json', {}, nil, 204 + end + end + + def setup_weather_not_found + ActiveResource::HttpMock.respond_to do |mock| + mock.get '/weather.json', {}, nil, 404 + end + end + + def setup_inventory + inventory = {:status => 'Sold Out', :total => 10, :used => 10}.to_json + + ActiveResource::HttpMock.respond_to do |mock| + mock.get '/products/5/inventory.json', {}, inventory + end + end + + def test_custom_singleton_name + assert_equal 'dashboard', WeatherDashboard.singleton_name + end + + def test_singleton_path + assert_equal '/weather.json', Weather.singleton_path + end + + def test_singleton_path_with_parameters + assert_equal '/weather.json?degrees=fahrenheit', Weather.singleton_path(:degrees => 'fahrenheit') + assert_equal '/weather.json?degrees=false', Weather.singleton_path(:degrees => false) + assert_equal '/weather.json?degrees=', Weather.singleton_path(:degrees => nil) + + assert_equal '/weather.json?degrees=fahrenheit', Weather.singleton_path('degrees' => 'fahrenheit') + + # Use include? because ordering of param hash is not guaranteed + path = Weather.singleton_path(:degrees => 'fahrenheit', :lunar => true) + assert path.include?('weather.json') + assert path.include?('degrees=fahrenheit') + assert path.include?('lunar=true') + + path = Weather.singleton_path(:days => ['monday', 'saturday and sunday', nil, false]) + assert_equal '/weather.json?days%5B%5D=monday&days%5B%5D=saturday+and+sunday&days%5B%5D=&days%5B%5D=false', path + + path = Inventory.singleton_path(:product_id => 5) + assert_equal '/products/5/inventory.json', path + + path = Inventory.singleton_path({:product_id =>5}, {:sold => true}) + assert_equal '/products/5/inventory.json?sold=true', path + end + + def test_find_singleton + setup_weather + weather = Weather.send(:find_singleton, Hash.new) + assert_not_nil weather + assert_equal 'Sunny', weather.status + assert_equal 67, weather.temperature + end + + def test_find + setup_weather + weather = Weather.find + assert_not_nil weather + assert_equal 'Sunny', weather.status + assert_equal 67, weather.temperature + end + + def test_find_with_param_options + setup_inventory + inventory = Inventory.find(:params => {:product_id => 5}) + + assert_not_nil inventory + assert_equal 'Sold Out', inventory.status + assert_equal 10, inventory.used + assert_equal 10, inventory.total + end + + def test_find_with_query_options + setup_weather + + weather = Weather.find(:params => {:degrees => 'fahrenheit'}) + assert_not_nil weather + assert_equal 'Sunny', weather.status + assert_equal 100, weather.temperature + end + + def test_not_found + setup_weather_not_found + + assert_raise ActiveResource::ResourceNotFound do + Weather.find + end + end + + def test_create_singleton + setup_weather + weather = Weather.create(:status => 'Sunny', :temperature => 67) + assert_not_nil weather + assert_equal 'Sunny', weather.status + assert_equal 67, weather.temperature + end + + def test_destroy + setup_weather + + # First Create the Weather + weather = Weather.create(:status => 'Sunny', :temperature => 67) + assert_not_nil weather + + # Now Destroy it + weather.destroy + end + + def test_update + setup_weather + + # First Create the Weather + weather = Weather.create(:status => 'Sunny', :temperature => 67) + assert_not_nil weather + + # Then update it + weather.status = 'Rainy' + weather.save + end +end +