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