Респондер

Респондер отвечает за преобразование ответа интерактора к 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))

Как видно интерактор имеет 2 успешных и 4 провальных типа ответа. Очевидно, реакция на случаи ::logged-out, ::not-authorized и ::not-found будет повторяться и для ответов других интеракторов.

Для обработки множества типов ответов удобно использовать мультиметод. При этом мультиметоды поддерживают наследование, что позволяет задать общие обработчики конкретным типам ответов.

(ns publicator.web.responders.base
  (:require
   [publicator.web.responses :as responses]
   [publicator.web.presenters.explain-data :as explain-data]
   [publicator.web.routing :as routing]))

(defmulti result->resp first)

(defmethod result->resp ::forbidden [_]
  {:status 403
   :headers {}
   :body "forbidden"})

(defmethod result->resp ::not-found [_]
  {:status 404
   :headers {}
   :body "not-found"})

(defmethod result->resp ::invalid-params [[_ explain-data]]
  (-> explain-data
      explain-data/->errors
      responses/render-errors))

(defmethod result->resp ::redirect-to-root [_]
  (responses/redirect-for-form (routing/path-for :pages/root)))

Здесь мы объявляем мультиметод, принимающий 2 аргумента: ответ интерактора и вектор аргументов. Также объявляются общие реализации для последующего связывания с конкретными ответами:

(ns publicator.web.responders.post.update
  (:require
   [publicator.use-cases.interactors.post.update :as interactor]
   [publicator.web.responders.base :as responders.base]
   [publicator.web.responses :as responses]
   [publicator.web.forms.post.params :as form]))

(defmethod responders.base/result->resp ::interactor/initial-params [[_ post params]]
  (let [form (form/build-update (:id post) params)]
    (responses/render-form form)))

(derive ::interactor/processed ::responders.base/redirect-to-root)
(derive ::interactor/invalid-params ::responders.base/invalid-params)
(derive ::interactor/logged-out ::responders.base/forbidden)
(derive ::interactor/not-authorized ::responders.base/forbidden)
(derive ::interactor/not-found ::responders.base/not-found)

Отмечу, что ответ с типом ::interactor/initial-params не содержит идентификатора поста, этот идентификатор извлекается из аргументов с которыми был вызван интерактор.

(ns publicator.web.responders.post.update-test
  (:require
   [publicator.utils.test.instrument :as instrument]
   [publicator.web.responders.post.update :as sut]
   [publicator.web.responders.base :as responders.base]
   [publicator.use-cases.test.factories :as factories]
   [publicator.use-cases.interactors.post.update :as interactor]
   [publicator.web.responders.shared-testing :as shared-testing]
   [ring.util.http-predicates :as http-predicates]
   [clojure.spec.alpha :as s]
   [clojure.test :as t]))

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

(t/deftest all-implemented
  (shared-testing/all-responders-are-implemented `interactor/initial-params)
  (shared-testing/all-responders-are-implemented `interactor/process))

(t/deftest initial-params
  (let [result (factories/gen ::interactor/initial-params)
        resp   (responders.base/result->resp result)]
    (t/is (http-predicates/ok? resp))))
(ns publicator.web.responders.shared-testing
  (:require
   [publicator.web.responders.base :as responders.base]
   [clojure.spec.alpha :as s]
   [clojure.test :as t]))

(defn all-responders-are-implemented [sym]
  (t/testing sym
    (let [[_ & pairs] (-> sym s/get-spec :ret s/describe)
          specs       (keep-indexed
                       (fn [idx item] (if (odd? idx) item))
                       pairs)
          implemented (-> responders.base/result->resp methods keys)]
      (doseq [spec specs]
        (t/testing spec
          (t/is (some #(isa? spec %) implemented) "not implemented"))))))