Skip to content

A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow

License

Notifications You must be signed in to change notification settings

lazy-cat-io/tenet

Repository files navigation

license https://github.com/lazy-cat-io/tenet/releases clojars

codecov build deploy

tenet

A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow.

Rationale

Problem statement

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)?

Solution

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.

Getting started

Add the following dependency in your project:

project.clj or build.boot
[io.lazy-cat/tenet "1.0.67"]
deps.edn or bb.edn
io.lazy-cat/tenet {:mvn/version "1.0.67"}

Limitations

This library doesn’t work with babashka (sci) - currently deftype is not supported.

Basic API

(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)

Anomaly registry

:busy
:conflict
:error
:forbidden
:incorrect
:interrupted
:not-found
:unauthorized
:unavailable
:unsupported

Response builders

Error response builders
(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)
Success response builders
(r/as-accepted x)
(r/as-created x)
(r/as-deleted x)
(r/as-found x)
(r/as-success x)
(r/as-updated x)
Custom response builders
(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

Examples

Basic API
(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}
Destructuring
(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}
Update response type
(-> (r/as-not-found 42)
    (r/as-incorrect)) ;; => #tenet [:incorrect 42]
Update response data
(-> (r/as-not-found 42)
    (r/as-incorrect)
    (update :data inc)) ;; => #tenet [:incorrect 43]

(-> (r/as-not-found {:foo {:bar 42}})
    (r/as-incorrect)
    (update-in [:data :foo :bar] inc)) ;; => #tenet [:incorrect {:foo {:bar 43}}]

Helper macros

(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!"]