Class: RuboCop::Cop::DevDoc::Route::NoCustomActions
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Route::NoCustomActions
- Defined in:
- lib/rubocop/cop/dev_doc/route/no_custom_actions.rb
Overview
Avoid custom ‘member` / `collection` actions; model them as RESTful sub-resources instead.
## Rationale Follow Rails’ standard REST principles as much as possible. A resource exposes seven standard actions (‘index`, `show`, `new`, `create`, `edit`, `update`, `destroy`); anything declared through a `member` or `collection` block is a custom verb bolted onto the resource. Each one is better expressed as its own RESTful sub-resource — the action then reads as a noun being created/destroyed, scopes cleanly under Pundit policies, and keeps every controller a thin CRUD controller.
❌ POST /products/3/activate
resources :products, only: [:show] do
member { post :activate }
end
✔️ POST /products/3/activations (create an activation)
resources :products, only: [:show] do
resources :activations, only: [:create]
end
✔️ a mode on the standard edit/update, when it's really one resource
PATCH /products/3?mode=activate
A reversible pair (‘activate` / `deactivate`, `lock` / `unlock`, `archive` / `restore`) maps naturally onto `create` / `destroy` of one sub-resource.
## Exception The doc says “as much as possible” — some custom actions are genuinely awkward to model as a sub-resource (a multi-step wizard step, a non-CRUD report endpoint). For those, disable the cop on the line with a written reason, e.g.:
member do
# rubocop:disable DevDoc/Route/NoCustomActions
get :balance # multi-step finalize wizard; not a persisted resource
# rubocop:enable DevDoc/Route/NoCustomActions
end
This cop flags every form a custom resource action takes:
- inside a `member` / `collection` block
- the inline `on: :member` / `on: :collection` option
(`get :activate, on: :member`)
- a bare verb directly inside a `resources` / `resource` block,
which Rails treats as a collection route (`resources :x do
get :search end`)
- a bare verb inside a `concern` block (its routes are mixed into
resources, so a custom verb there is a custom resource action)
‘constraints` / `defaults` wrappers between the verb and its resource are transparent — a custom action nested inside them is still caught.
NOTE: Stand-alone non-resource routes (‘get ’sitemap.xml’‘, `get ’proxy’‘) and verbs scoped by a `namespace` / `scope` block are not resourceful, and are intentionally left alone. A custom action written as a flat top-level route (`get ’photos/search’, to: ‘photos#search’‘) is indistinguishable from such a route and cannot be flagged without false positives — that case is left to review.
Constant Summary collapse
- MSG =
'Custom `%<context>s` action `%<verb>s %<name>s`. Model it as a RESTful ' \ 'sub-resource (e.g. `resources :activations, only: [:create]`) instead. ' \ 'Disable with a reason if a custom action is genuinely unavoidable.'.freeze
- RESTRICT_ON_SEND =
%i[get post patch put delete match].freeze
- MEMBER_OR_COLLECTION =
%i[member collection].freeze
- RESOURCEFUL =
%i[resources resource].freeze
- TRANSPARENT_WRAPPERS =
Wrappers that add conditions but do not change whether the verb is a resource action — walk through them to find the real context.
%i[constraints defaults].freeze
Instance Method Summary collapse
Instance Method Details
#on_send(node) ⇒ Object
92 93 94 95 96 97 98 99 100 |
# File 'lib/rubocop/cop/dev_doc/route/no_custom_actions.rb', line 92 def on_send(node) context = routing_context(node) return unless context add_offense( node.loc.selector, message: format(MSG, context: context, verb: node.method_name, name: action_name(node)) ) end |