Controller

Контроллер - адаптер для интерактора, который конвертирует ring запрос в данные, понятные интерактору. Также контроллер содержит объявление маршрутов.

Контроллер - название довольно условное, и в своем проекте вы можете использовать другое.

Рассмотрим контроллер для сценария обновления поста. Напомню спецификации функций интерактора:

(s/fdef initial-params
  :args (s/cat :id ::post/id)
  :ret (s/or :ok  ::initial-params
             :err ::logged-out
             :err ::not-authorized
             :err ::not-found))

(s/fdef process
  :args (s/cat :id ::post/id
               :params any?)
  :ret (s/or :ok  ::processed
             :err ::logged-out
             :err ::not-authorized
             :err ::not-found
             :err ::invalid-params))

Мы должны показать пользователю форму и обработать данные этой формы.

Назовем экшены контроллера аналогично функциям интерактора: initial-params и process.

(ns publicator.web.controllers.post.update
  (:require
   [publicator.use-cases.interactors.post.update :as interactor]))

(defn- req->id [req]
  (-> req
      :route-params
      :id
      Integer.))

(defn initial-params [req]
  (let [id (req->id req)]
    [interactor/initial-params id]))

(defn process [{:keys [transit-params] :as req}]
  (let [id (req->id req)]
    [interactor/process id transit-params]))

(def routes
  #{[:get "/posts/:id{\\d+}/edit" #'initial-params :post.update/initial-params]
    [:post "/posts/:id{\\d+}/edit" #'process :post.update/process]})

Ключ :route-params добавляет библиотека роутинга, он содержит url параметры, в нашем случае - id.

Форма отправляет данные в формате transit. Соответствующая middleware добавляет ключ :transit-params с расшифрованными данными формы.

Тот факт, что контроллер не вызывает интерактор позволяет не подменять интерактор заглушкой и не реализовывать повторно сценарии тестирования интерактора:

(ns publicator.web.controllers.post.update-test
  (:require
   [publicator.utils.test.instrument :as instrument]
   [publicator.web.controllers.post.update :as sut]
   [publicator.use-cases.interactors.post.update :as interactor]
   [publicator.use-cases.test.factories :as factories]
   [ring.mock.request :as mock.request]
   [clojure.test :as t]
   [clojure.spec.alpha :as s]
   [sibiro.core]
   [sibiro.extras]))

(t/use-fixtures :once instrument/fixture)

(def handler
  (-> sut/routes
      sibiro.core/compile-routes
      sibiro.extras/make-handler))

(t/deftest initial-params
  (let [req             (mock.request/request :get "/posts/1/edit")
        [action & args] (handler req)
        args-spec       (-> `interactor/initial-params s/get-spec :args)]
    (t/is (= interactor/initial-params action))
    (t/is (nil? (s/explain-data args-spec args)))))

(t/deftest process
  (let [params          (factories/gen ::interactor/params)
        req             (-> (mock.request/request :post "/posts/1/edit")
                            (assoc :transit-params params))
        [action & args] (handler req)
        args-spec       (-> `interactor/process s/get-spec :args)]
    (t/is (= interactor/process action))
    (t/is (nil? (s/explain-data args-spec args)))))

Проверяем роутинг. Проверяем правильность возвращаемой функции интерактора, а также соответствие полученных аргументов интерактора их спецификации.

Кроме контроллеров - адаптеров интеракторов, в веб приложении есть потребность в обычных страницах. Экшены таких контроллеров возвращают не вектор, как описывалось ранее, а обычный ring ответ:

(ns publicator.web.controllers.pages.root
  (:require
   [publicator.web.responses :as responses]))

(defn show [_]
  (responses/render-page "pages/root" {}))

(def routes
  #{[:get "/" #'show :pages/root]})

В предыдущем параграфе мы видели middleware, оборачивающую экшены контроллеров:

(defn middleware [handler]
  (fn [req]
    (let [[interactor & args] (handler req)
          result              (apply interactor args)]
      (responder result args))))

Для того, чтобы обрабатывать обычные ring ответы добавим соответствующее условие:

(ns publicator.web.middlewares.responder
  (:require
   [publicator.web.responders.base :as responders.base]
   ;; ..
   ))

(defn wrap-reponder [handler]
  (fn [req]
    (let [resp (handler req)]
      (if (vector? resp)
        (let [[interactor & args] resp
              result              (apply interactor args)]
          (responders.base/result->resp result))
        resp))))

Самостоятельно просмотрите все контролеры.