Skip to content

Singletons: Default collection_name to singleton_name #418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

seanpdoyle
Copy link

@seanpdoyle seanpdoyle commented Jan 13, 2025

The problem

Active Resource's custom methods are operations outside of the
conventional collection of CRUD actions. For typical collections of
resources, custom methods operate on the collection itself. For example,
consider a custom refresh action for a Product resource triggered by
POST requests: POST /products/:product_id/refresh. The route
utilizes a :product_id dynamic segment to identify the Product in
question, and the path includes /refresh to signify the custom method
to perform.

The concept of a "singleton" resource is that there is only one singular
resource, and operations should consistently modify the same resource.
Since the resource is singular in nature, paths do not identify that
resource with an ID. For example, consider a singleton Inventory
resource that belongs to a Product. Its singleton path would be
/products/:product_id/inventory.

Prior to this commit, custom methods invoked by both instances and
classes of singleton resources ignored the "singleton" nature of
route, and use pluralize nouns instead of singular ones.

For example, consider calls to custom "report" and "reset" methods for
an instance of a singleton Inventory resource:

inventory = Inventory.find(params: { product_id: 1 }) # => GET /products/1/inventory.json

  # BEFORE
inventory.get(:report, product_id: 1)   # => GET /products/1/inventories/report.json
inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventories/reset.json

Note the /inventories/ portion of the URL prefix. The same occurs for
class methods. For example, consider calls to the same custom "report"
and "reset" routes for a singleton Inventory resource class:

 # BEFORE
Inventory.get(:report, product_id: 1)   # => GET /products/1/inventories/report.json
Inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventories/reset.json

The proposal

In order to make "singleton" resources behave more consistently with a
singular mental model, this commit proposes that Active Resource change
instance-level custom methods (through the same get, post, put,
patch, and delete style methods) to use the singular singleton name
in their paths.

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.

  # AFTER
Inventory.get(:report, product_id: 1)   # => GET /products/1/inventory/report.json
Inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventory/reset.json

When a collection_name is explicitly configured, use that value
instead of the singleton_name default.

Apply the same changes to instances of singleton resources:

inventory = Inventory.find(params: { product_id: 1 }) # => GET /products/1/inventory.json

  # AFTER
inventory.get(:report)    # => GET /products/1/inventory/report.json
inventory.delete(:reset)  # => DELETE /products/1/inventory/reset.json

Comment on lines +134 to +136
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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is copied directly from ActiveResource::CustomMethods, then altered to omit the id portion of the path:

def custom_method_element_url(method_name, options = {})
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{URI.encode_www_form_component(id.to_s)}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
end

 def custom_method_element_url(method_name, options = {})
-  "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{URI.encode_www_form_component(id.to_s)}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
+  "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{method_name}#{self.class.format_extension}#{self.class.__send__(:query_string, options)}"
 end

The problem
---

Active Resource's custom methods are operations outside of the
conventional collection of CRUD actions. For typical collections of
resources, custom methods operate on the collection itself. For example,
consider a custom `refresh` action for a `Product` resource triggered by
`POST` requests: `POST /products/:product_id/refresh`. The route
utilizes a `:product_id` dynamic segment to identify the `Product` in
question, and the path includes `/refresh` to signify the custom method
to perform.

The concept of a "singleton" resource is that there is only one singular
resource, and operations should consistently modify the same resource.
Since the resource is singular in nature, paths *do not* identify that
resource with an ID. For example, consider a singleton `Inventory`
resource that belongs to a `Product`. Its singleton path would be
`/products/:product_id/inventory`.

Prior to this commit, custom methods invoked by both *instances* and
*classes* of singleton resources ignored the "singleton" nature of
route, and use pluralize nouns instead of singular ones.

For example, consider calls to custom "report" and "reset" methods for
an instance of a singleton `Inventory` resource:

```ruby
inventory = Inventory.find(params: { product_id: 1 }) # => GET /products/1/inventory.json

  # BEFORE
inventory.get(:report, product_id: 1)   # => GET /products/1/inventories/report.json
inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventories/reset.json
```

Note the `/inventories/` portion of the URL prefix. The same occurs for
class methods. For example, consider calls to the same custom "report"
and "reset" routes for a singleton `Inventory` resource class:

```ruby
 # BEFORE
Inventory.get(:report, product_id: 1)   # => GET /products/1/inventories/report.json
Inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventories/reset.json
```

The proposal
---

In order to make "singleton" resources behave more consistently with a
singular mental model, this commit proposes that Active Resource change
instance-level custom methods (through the same `get`, `post`, `put`,
`patch`, and `delete` style methods) to use the singular singleton name
in their paths.

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
  # AFTER
Inventory.get(:report, product_id: 1)   # => GET /products/1/inventory/report.json
Inventory.delete(:reset, product_id: 1) # => DELETE /products/1/inventory/reset.json
```

When a `collection_name` is explicitly configured, use that value
instead of the `singleton_name` default.

Apply the same changes to instances of singleton resources:

```ruby
inventory = Inventory.find(params: { product_id: 1 }) # => GET /products/1/inventory.json

  # AFTER
inventory.get(:report)    # => GET /products/1/inventory/report.json
inventory.delete(:reset)  # => DELETE /products/1/inventory/reset.json
```
@seanpdoyle seanpdoyle force-pushed the singleton-custom-methods branch from d82b7cd to 213f735 Compare July 18, 2025 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant