Skip to the content.

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::ProductsControllerProduct). 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

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.