A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow.
Usually, when working in a team, it is necessary to agree beforehand on the type of results to be used.
Someone uses maps, someone uses vectors, someone uses monads like Either
, Maybe
, etc.
It is not always clear when a function returns data without some context (e.g. nil
, 42
, etc).
What does nil
mean: No data?
Didn’t do anything?
Did something go wrong?
What does 42
mean: User id?
Age?
Such answers make you look at the current implementation and spend time understanding the current context.
Imagine that we have a function containing some kind of business logic:
(defn create-user! [user]
(if-not (valid? user)
;; returns the response that the data is not valid
(if-not (exists? user)
;; returns the response that the email is occupied
(db/insert! user)))) ;; returns the response that the user was created or an error occurred while writing data to the database
In this case, there may be several possible responses:
-
the user data is not valid
-
the email is occupied
-
an error occurred while writing data to the database
-
or finally, a response about a successful operation: e.g. user id or data
There is a useful data type in the Clojure - a keyword
that can be used to add some context to the response:
-
:user/incorrect
,:user/exists
-
:user/created
or:org.acme.user/created
Having such an answer, it is immediately clear what exactly happened - we have the context and data. Most of the time we do not write code, we read it. And this is very important.
We have decided on the context, but how to add it? Key-value in the map? Vector? Monad? Metadata? And how to understand which answer is considered an error (anomaly)?
We used all the above methods in our practice, and it has always been something inconvenient.
What should be the structure of the map, vector? Create custom object/type and use getters and setters? This adds problems in further use and looks like OOP. Use metadata? Metadata cannot be added to some types of data. And what type of response is considered an error (anomaly)?
This library helps to unify responses and anomalies. Out of the box, there are general types of responses and all the necessary helper functions.
In short, all the responses are a Pair [type data]
.
E.g. [:org.acme.user/created {:user/id 42}]
.
There are no requirements for the type of response and the type of data. Always the same data structure.
We have a registry of basic anomalies to which you can add your own type, or you can use the global hierarchy using derive
from the parent :tenet.response/error
.
The registry was added to increase the performance.
Checking an anomaly in the registry takes ~15-25 ns, and checking using the global hierarchy takes ~120-150 ns.
See the performance tests.
Add the following dependency in your project:
[io.lazy-cat/tenet "1.0.67"]
io.lazy-cat/tenet {:mvn/version "1.0.67"}
This library doesn’t work with babashka (sci) - currently deftype
is not supported.
(ns example
(:require
[tenet.response :as r]))
(r/anomaly? x)
;; Exception and js/Error are anomalies
;; Object, nil, default are not anomalies
;; Other data types are anomalies if they are registered in the registry or inherited from `:tenet.response/error`
(r/as-response x :your-response-type)
:busy
:conflict
:error
:forbidden
:incorrect
:interrupted
:not-found
:unauthorized
:unavailable
:unsupported
(r/as-busy x)
(r/as-conflict x)
(r/as-error x)
(r/as-forbidden x)
(r/as-incorrect x)
(r/as-interrupted x)
(r/as-not-found x)
(r/as-unauthorized x)
(r/as-unavailable x)
(r/as-unsupported x)
(r/as-accepted x)
(r/as-created x)
(r/as-deleted x)
(r/as-found x)
(r/as-success x)
(r/as-updated x)
(derive :org.acme.user/incorrect ::r/error) ;; or (swap! r/*registry conj :org.acme.user/incorrect)
(r/as :org.acme.user/incorrect :foo) ;; => #tenet [:org.acme.user/incorrect :foo]
(-> (r/as :org.acme.user/incorrect) (r/anomaly?)) ;; => true
(-> (r/as :org.acme.user/incorrect :foo) (r/anomaly?)) ;; => true
(r/as :org.acme.user/created :foo) ;; => #tenet [:org.acme.user/created :foo]
(-> (r/as :org.acme.user/created :foo) (r/anomaly?)) ;; => false
(r/as-not-found 42) ;; => #tenet [:not-found 42]
(r/anomaly? (r/as-not-found 42)) ;; => true
(r/anomaly? (r/as-created 42)) ;; => false
(:type (r/as-created 42)) ;; => :created
(:data (r/as-created 42)) ;; => 42
@(r/as-created 42) ;; => 42
(-> (r/as-created 42)
(with-meta {:foo :bar})
(meta)) ;; => {:foo :bar}
(let [[type data] (r/as-not-found 42)]
{:type type, :data data}) ;; => {:type :not-found, :data 42}
(let [{:keys [type data]} (r/as-not-found 42)]
{:type type, :data data}) ;; => {:type :not-found, :data 42}
(def boom!
(constantly :error))
;; just like `some->`, but checks for anomalies
(r/-> 42 inc) ;; => 43
(r/-> 42 inc boom!) ;; => :error
(r/-> 42 inc boom! inc) ;; => :error
;; just like `some->>`, but checks for anomalies
(r/->> 42 inc) ;; => 43
(r/->> 42 inc boom!) ;; => :error
(r/->> 42 inc boom! inc) ;; => :error
;; handle exceptions
(r/safe (Exception. "boom!")) ;; => nil
(r/safe (Exception. "boom!") #(r/as-error (ex-message %))) ;; => #tenet [:error "boom!"]