Controllers
Source: lib/cafe_car/controller.rb (the CafeCar::Controller concern — usually
included in the host’s ApplicationController by the installer).
The macro
module Admin
class ProductsController < ApplicationController
cafe_car
end
end
cafe_car wires the full CRUD surface: index show new edit create update destroy,
plus batch (bulk actions), options (association-select typeahead JSON), and the
generic member_action / collection_action endpoints. It
authorizes every action through Pundit (verify_authorized and
verify_policy_scoped are enforced), responds to :html, :json, :turbo_stream,
and :csv, rescues validation failures into a re-rendered form, and appends the
engine’s view fallbacks.
The model is inferred from the controller name (Admin::ProductsController →
Product). Variants:
cafe_car only: %i[index show] # limit actions — excluded ones respond 404
cafe_car except: %i[destroy]
cafe_car model: Company # or the standalone `model Company` macro
class AttachmentsController < ApplicationController
cafe_car
model ::ActiveStorage::Attachment # point at any model, even a library's
default_view :grid # index defaults to grid instead of table
end
only:/except: gate the whole surface — the RESTful seven plus batch,
options, member_action, and collection_action. Excluded actions respond
404, exactly as if their routes weren’t drawn; mirror the narrowing in the
routes (cafe_car :products, only: %i[index show]) so they aren’t.
The scope pipeline
Every action reads records through one method:
def scope = model.all.then { policy_scope _1 }
.then { sorted _1 }
.then { filtered _1 }
.then { eager_loaded _1 }
.then { paginated _1 }
policy_scope applies the policy’s Scope#resolve; sorted reads ?sort=;
filtered reads the dot-query params and ?q=; eager_loaded preloads displayed
associations (no N+1); paginated is Kaminari (?page=, ?per= capped at
CafeCar.max_per_page). CSV requests skip pagination and export the whole
filtered set.
Narrow one controller without touching the policy:
class Admin::PublishedArticlesController < ApplicationController
cafe_car model: Article
def scope = super.published
end
Prefer the policy Scope when the restriction is about who may see what; override
scope when it’s about what this particular screen lists.
Objects and callbacks
object / objects are the current record/collection, also exposed as the
conventional ivars (@product, @products). Lifecycle callbacks exist for
:create, :update, :destroy, and :render:
class ProductsController < ApplicationController
cafe_car
after_create do
NotificationMailer.product_created(object).deliver_later
end
end
(before_/around_/after_ + skip_* helpers are defined for each; blocks run in
controller context around object.save! / object.destroy!.)
Extra endpoints
Draw a CafeCar resource’s routes with the cafe_car macro — resources plus
four endpoints (lib/cafe_car/routing.rb); a plain resources call draws
exactly Rails’ routes and gains nothing:
# config/routes.rb
cafe_car :products # resources :products + the four below
cafe_car :products, only: %i[index show] # only:/except: filter these too
POST /products/batch— applies a bulk action to selected ids. The action name must be in the policy’spermitted_bulk_actions; each record is authorized individually against<action>?then receives<action>!. See policies.md.GET /products/options?q=…— policy-scoped[{value, text}]JSON feeding the searchable association selects. See forms.md.POST /products/:id/actions/:member_action— runs a name listed bypermitted_member_actions. CafeCar authorizes<name>?, then calls the record’s<name>!. A public controller method named<name>replaces the forwarding and owns the response.POST /products/actions/:collection_action— runs a name listed bypermitted_collection_actionsover the currently viewed, filtered scope. CafeCar authorizes<name>?against the model class, then calls<name>!on that scope. A public controller method named<name>may instead scope its own query and respond directly.
The policy lists the names; hosts do not enumerate custom actions in routes. See policies.md for the model and policy conventions.
Responses
Success responds per format: HTML redirects with a locale-driven flash
(flashes.create_html etc.), turbo_stream emits a page refresh (morph — see
turbo.md), JSON serializes [:id] + attributes.displayable per the
policy. Validation failure re-renders new/edit with 422. Authorization failure
returns 403 — or redirects to login when the opt-in sessions are installed
(bin/rails g cafe_car:sessions).
To customize one response, replace the action without calling super first:
def create
run_callbacks(:create) { object.save! }
respond_with object, location: product_setup_path(object)
end
Calling super already sends CafeCar’s response; responding again risks a double
render.