Skip to content

Commit d82b7cd

Browse files
committed
Singletons: Default collection_name to singleton_name
When declaring a resource as a "singleton" (through including the `ActiveResource::Singleton` module), ensure that subsequent calls to class-level custom methods (through the `get`, `post`, `put`, `patch`, and `delete` class and instance methods) use the singleton name by default. ```ruby Inventory.get(:report, product_id: 1) # => GET /products/1/inventory/report.json Inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventory/reset.json ``` Similarly, instance-level custom methods (through the same `get`, `post`, `put`, `patch`, and `delete` style methods) use the singleton name **and** omit the resource ID from the path. ```ruby inventory = Inventory.find(params: { product_id: 1 }) # => GET /products/1/inventory.json inventory.get(:report) # => GET /products/1/inventory/report.json inventory.delete(:reset) # => DELETE /products/1/inventory/reset.json ``` When a `collection_name` is explicitly configured, use that value instead of the `singleton_name` default.
1 parent 9c8a2ee commit d82b7cd

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

lib/active_resource/singleton.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
# frozen_string_literal: true
22

33
module ActiveResource
4+
# === Custom REST methods
5+
#
6+
# Since simple CRUD/life cycle methods can't accomplish every task, Singleton Resources can also support
7+
# defining custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
8+
# <tt>post</tt>, <tt>put</tt> and <tt>delete</tt> methods where you can specify a custom REST method
9+
# name to invoke.
10+
#
11+
# Singleton resources use their <tt>singleton_name</tt> value as their default
12+
# <tt>collection_name</tt> value when constructing the request's path.
13+
#
14+
# # GET to report on the Inventory, i.e. GET /products/1/inventory/report.json.
15+
# Inventory.get(:report, product_id: 1)
16+
# # => [{:count => 'Manager'}, {:name => 'Clerk'}]
17+
#
18+
# # DELETE to 'reset' an inventory, i.e. DELETE /products/1/inventory/reset.json.
19+
# Inventory.find(params: { product_id: 1 }).delete(:reset)
20+
#
21+
# For more information on using custom REST methods, see the
22+
# ActiveResource::CustomMethods documentation.
423
module Singleton
524
extend ActiveSupport::Concern
625

@@ -11,6 +30,10 @@ def singleton_name
1130
@singleton_name ||= model_name.element
1231
end
1332

33+
def collection_name
34+
@collection_name ||= singleton_name
35+
end
36+
1437
# Gets the singleton path for the object. If the +query_options+ parameter is omitted, Rails
1538
# will split from the \prefix options.
1639
#
@@ -107,5 +130,9 @@ def create
107130
def singleton_path(options = nil)
108131
self.class.singleton_path(options || prefix_options)
109132
end
133+
134+
def custom_method_element_url(method_name, options = {})
135+
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
136+
end
110137
end
111138
end

test/cases/base/custom_methods_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "abstract_unit"
4+
require "fixtures/inventory"
45
require "fixtures/person"
56
require "fixtures/street_address"
67
require "active_support/core_ext/hash/conversions"
@@ -13,6 +14,8 @@ def setup
1314
@ryan = { person: { name: "Ryan" } }.to_json
1415
@addy = { address: { id: 1, street: "12345 Street" } }.to_json
1516
@addy_deep = { address: { id: 1, street: "12345 Street", zip: "27519" } }.to_json
17+
@inventory = { inventory: { id: 1, name: "Warehouse" } }.to_json
18+
@inventory_array = { inventory: [{ id: 1, name: "Warehouse" }] }.to_json
1619

1720
ActiveResource::HttpMock.respond_to do |mock|
1821
mock.get "/people/1.json", {}, @matz
@@ -33,6 +36,13 @@ def setup
3336
mock.put "/people/1/addresses/1/normalize_phone.json?locale=US", {}, nil, 204
3437
mock.put "/people/1/addresses/sort.json?by=name", {}, nil, 204
3538
mock.post "/people/1/addresses/new/link.json", {}, { address: { street: "12345 Street" } }.to_json, 201, "Location" => "/people/1/addresses/2.json"
39+
mock.get "/products/1/inventory.json", {}, @inventory
40+
mock.get "/products/1/inventory/shallow.json", {}, @inventory
41+
mock.get "/products/1/inventory/retrieve.json?name=Warehouse", {}, @inventory_array
42+
mock.post "/products/1/inventory/purchase.json?name=Warehouse", {}, nil, 201
43+
mock.put "/products/1/inventory/promote.json?name=Warehouse", {}, nil, 204
44+
mock.put "/products/1/inventory/sort.json?by=name", {}, nil, 204
45+
mock.delete "/products/1/inventory/deactivate.json", {}, nil, 200
3646
end
3747

3848
Person.user = nil
@@ -97,6 +107,32 @@ def test_custom_new_element_method
97107
assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register)
98108
end
99109

110+
def test_singleton_custom_collection_method
111+
# GET
112+
assert_equal([{ "id" => 1, "name" => "Warehouse" }], Inventory.get(:retrieve, product_id: 1, name: "Warehouse"))
113+
114+
# POST
115+
assert_equal(ActiveResource::Response.new("", 201, {}), Inventory.post(:purchase, product_id: 1, name: "Warehouse"))
116+
117+
# PUT
118+
assert_equal ActiveResource::Response.new("", 204, {}),
119+
Inventory.put(:promote, { product_id: 1, name: "Warehouse" }, "atestbody")
120+
assert_equal ActiveResource::Response.new("", 204, {}), Inventory.put(:sort, product_id: 1, by: "name")
121+
end
122+
123+
def test_singleton_custom_element_method
124+
inventory = Inventory.find(params: { product_id: 1 })
125+
126+
# Test GET against an element URL
127+
assert_equal inventory.get(:shallow), "id" => 1, "name" => "Warehouse"
128+
129+
# Test PUT against an element URL
130+
assert_equal ActiveResource::Response.new("", 204, {}), inventory.put(:promote, { name: "Warehouse" }, "body")
131+
132+
# Test DELETE against an element URL
133+
assert_equal ActiveResource::Response.new("", 200, {}), inventory.delete(:deactivate)
134+
end
135+
100136
def test_find_custom_resources
101137
assert_equal "Matz", Person.find(:all, from: :managers).first.name
102138
end

0 commit comments

Comments
 (0)