diff --git a/lib/active_resource/singleton.rb b/lib/active_resource/singleton.rb index 4143a85a7b..5034563a86 100644 --- a/lib/active_resource/singleton.rb +++ b/lib/active_resource/singleton.rb @@ -1,6 +1,25 @@ # frozen_string_literal: true module ActiveResource + # === Custom REST methods + # + # Since simple CRUD/life cycle methods can't accomplish every task, Singleton Resources can also support + # defining custom REST methods. To invoke them, Active Resource provides the get, + # post, put and delete methods where you can specify a custom REST method + # name to invoke. + # + # Singleton resources use their singleton_name value as their default + # collection_name value when constructing the request's path. + # + # # GET to report on the Inventory, i.e. GET /products/1/inventory/report.json. + # Inventory.get(:report, product_id: 1) + # # => [{:count => 'Manager'}, {:name => 'Clerk'}] + # + # # DELETE to 'reset' an inventory, i.e. DELETE /products/1/inventory/reset.json. + # Inventory.find(params: { product_id: 1 }).delete(:reset) + # + # For more information on using custom REST methods, see the + # ActiveResource::CustomMethods documentation. module Singleton extend ActiveSupport::Concern @@ -11,6 +30,10 @@ def singleton_name @singleton_name ||= model_name.element end + def collection_name + @collection_name || singleton_name + end + # Gets the singleton path for the object. If the +query_options+ parameter is omitted, Rails # will split from the \prefix options. # @@ -107,5 +130,9 @@ def create def singleton_path(options = nil) self.class.singleton_path(options || prefix_options) end + + def custom_method_element_url(method_name, options = {}) + "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}" + end end end diff --git a/test/cases/base/custom_methods_test.rb b/test/cases/base/custom_methods_test.rb index edac3278c8..d18afccc5d 100644 --- a/test/cases/base/custom_methods_test.rb +++ b/test/cases/base/custom_methods_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "abstract_unit" +require "fixtures/inventory" require "fixtures/person" require "fixtures/street_address" require "active_support/core_ext/hash/conversions" @@ -13,6 +14,8 @@ def setup @ryan = { person: { name: "Ryan" } }.to_json @addy = { address: { id: 1, street: "12345 Street" } }.to_json @addy_deep = { address: { id: 1, street: "12345 Street", zip: "27519" } }.to_json + @inventory = { inventory: { id: 1, name: "Warehouse" } }.to_json + @inventory_array = { inventory: [{ id: 1, name: "Warehouse" }] }.to_json ActiveResource::HttpMock.respond_to do |mock| mock.get "/people/1.json", {}, @matz @@ -33,6 +36,13 @@ def setup mock.put "/people/1/addresses/1/normalize_phone.json?locale=US", {}, nil, 204 mock.put "/people/1/addresses/sort.json?by=name", {}, nil, 204 mock.post "/people/1/addresses/new/link.json", {}, { address: { street: "12345 Street" } }.to_json, 201, "Location" => "/people/1/addresses/2.json" + mock.get "/products/1/inventory.json", {}, @inventory + mock.get "/products/1/inventory/shallow.json", {}, @inventory + mock.get "/products/1/inventory/retrieve.json?name=Warehouse", {}, @inventory_array + mock.post "/products/1/inventory/purchase.json?name=Warehouse", {}, nil, 201 + mock.put "/products/1/inventory/promote.json?name=Warehouse", {}, nil, 204 + mock.put "/products/1/inventory/sort.json?by=name", {}, nil, 204 + mock.delete "/products/1/inventory/deactivate.json", {}, nil, 200 end Person.user = nil @@ -97,6 +107,43 @@ def test_custom_new_element_method assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register) end + def test_singleton_custom_collection_method + # GET + assert_equal([{ "id" => 1, "name" => "Warehouse" }], Inventory.get(:retrieve, product_id: 1, name: "Warehouse")) + + # POST + assert_equal(ActiveResource::Response.new("", 201, {}), Inventory.post(:purchase, product_id: 1, name: "Warehouse")) + + # PUT + assert_equal ActiveResource::Response.new("", 204, {}), + Inventory.put(:promote, { product_id: 1, name: "Warehouse" }, "atestbody") + assert_equal ActiveResource::Response.new("", 204, {}), Inventory.put(:sort, product_id: 1, by: "name") + end + + def test_singleton_custom_collection_method_with_overridden_collection_name + original_collection_name, Inventory.collection_name = Inventory.collection_name, "special_inventory" + + ActiveResource::HttpMock.respond_to.get "/products/1/special_inventory/shallow.json", {}, @inventory + + # GET + assert_equal({ "id" => 1, "name" => "Warehouse" }, Inventory.get(:shallow, product_id: 1)) + ensure + Inventory.collection_name = original_collection_name + end + + def test_singleton_custom_element_method + inventory = Inventory.find(params: { product_id: 1 }) + + # Test GET against an element URL + assert_equal inventory.get(:shallow), "id" => 1, "name" => "Warehouse" + + # Test PUT against an element URL + assert_equal ActiveResource::Response.new("", 204, {}), inventory.put(:promote, { name: "Warehouse" }, "body") + + # Test DELETE against an element URL + assert_equal ActiveResource::Response.new("", 200, {}), inventory.delete(:deactivate) + end + def test_find_custom_resources assert_equal "Matz", Person.find(:all, from: :managers).first.name end