Class: RuboCop::Cop::DevDoc::Route::NoCustomActions

Inherits:
Base
  • Object
show all
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.

Examples:

# bad
resources :products, only: [:show] do
  member do
    post :activate
    post :deactivate
  end
end

# good
resources :products, only: [:show] do
  resources :activations, only: %i[create destroy]
end

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