avatar

Krisztián Gulyás

Posted on 7th November 2022

Xiana – Monads removed

news-paper Clojure | News | Software Development |

A couple of months ago I wrote about monads in the Xiana framework.
Now, I’ll write about why we removed it, what we lost, and what we got in exchange.

The problem

Xiana flow is based on interceptors. It has pairs of functions for the pair of functionality like JWT encode and decode,
session restore and update, request and response coercion, etc. When a request arrives, it flows through on :enter
functions, reaches the action and goes backward through :leave functions. Monads are designed for straight lines,
indeed rail-like execution. It’s not designed for turning back while the data flows. As I already mentioned in the
previous article, the execution flow stops when an unhandled exception appears. No response validation, no encoding,
and nothing else but what the interceptor or the executor associates into the response key.

Options

Using finally branch

Right now we have two interceptor branches. One is executed around the router, and the second is right after it, around
the action. There was an option to add a third branch, which is executed in case of exception, which contains only the
necessary interceptors to handle the must-have functions, like session update, formatting response, or JWT encoding the
body. It leads to code duplication because you need to repeat those interceptors which are in the flow already.

Changing the execution flow

There is always an option to learn from others. We can re-design our flow to behave similarly to other frameworks. Stas came up with the idea, to have something
like metosin/sieppari for error handling.

Decision

It was a long and hard discussion, and we choose the second option. We wanted to be sure that we are not losing any
already provided functionality, still easy to use, simple and clear how the work has been done under the hood. And there
was another thing.

Do we still need Monads?

And the answer was no, we don’t need monads, indeed they are blocking us to move on. The new executor flow doesn’t need
extra protection. We lost a functionality here, namely resuming from an exception. But we still have the option to
handle the exception where it occurs, and we got two new features.

What we got in exchange

The execution flow turns back in case of exception

If your interceptor has an `:error` function, all the previously executed interceptors will be called in `:leave`
branch, instead of returning the raw exception.

Optional universal exception handler for handling all types of exceptions

A well-placed interceptor with :error function is able to catch and process all exceptions. In this case, all
interceptors defined before it will be executed with those :leave functions. It makes it possible to have a finally
branch without repeating the same interceptors in the application configuration.

Flow like Sieppari

Maybe it sounds strange, but working and understanding a flow that another big competitor has is a good enhancement.
Maybe this is not the simplest way to do things, but our users can profit from it. Our goal is to have a simple
starting point for developers, who are new to Clojure development, but if they going on another project they will
already know how Sieppari works. No need to learn the same thing twice.

Less mental strain

With the removal of monads, the usage of the framework went easier. Without another level of abstraction, the
developer is able to focus more on the product, instead of technical musts.

See an example for simplified code

This code show how an example login is implemented originally, and in comments I’ll show you how it is changed:

(ns app.example.login
  (:require
    [clojure.data.json :as json]
    [xiana.session :as session]
    [ring.util.request :refer [body-string]]
    [xiana.core :as xiana])
  ;(:require
  ;  [clojure.data.json :as json]
  ;  [xiana.session :as session]
  ;  [ring.util.request :refer [body-string]])
  (:import
    (java.util
      UUID)))

(def db
  [{:id         1
    :email      "piotr@example.com"
    :first-name "Piotr"
    :last-name  "Developer"
    :password   "topsecret"}])

(defn find-user
  [email]
  (first (filter (fn [i]
                   (= email (:email i))) db)))

(defn missing-credentials
  [state]
  (xiana/error (assoc state :response {:status 401
                                       :body   "Missing credentials"})
  ;(assoc state :response {:status 401
  ;                        :body   "Missing credentials"})
  ))

(defn login
  [{request :request :as state}]
  (try (let [rbody (or (some-> request
                               body-string
                               (json/read-str :key-fn keyword))
                       (throw (ex-message "Missing body")))
             user (find-user (-> rbody :email))
             session-id (UUID/randomUUID)
             session-data {:session-id session-id
                           :user       (dissoc user :password)}]
         (if (and user (= (:password user) (:password rbody)))
           (let [session-backend (get-in state [:deps :session-backend])]
             (session/add! session-backend session-id session-data)
             (xiana/ok (assoc state
                              :response {:status  200
                                         :headers {"Content-Type" "application/json"
                                                   "Session-id"   (str session-id)}
                                         :body    (json/write-str (update session-data :session-id str))})))
           ;(assoc state
           ;  :response {:status  200
           ;             :headers {"Content-Type" "application/json"
           ;                       "Session-id"   (str session-id)}
           ;             :body    (json/write-str (update session-data :session-id str))})

           (xiana/error (assoc state :response {:status 401
                                                :body   "Incorrect credentials"}))
           ;(assoc state :response {:status 401
           ;                        :body   "Incorrect credentials"})
           ))
       (catch Exception _ (missing-credentials state))))

(defn login-controller
  [state]
  (xiana/flow-> state
                login)
  ;(login state)
  )