Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/lazy-cat-io/tenet
A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow
https://github.com/lazy-cat-io/tenet
clojure clojurescript data-flow response tenet unified-response unifier
Last synced: 2 days ago
JSON representation
A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow
- Host: GitHub
- URL: https://github.com/lazy-cat-io/tenet
- Owner: lazy-cat-io
- License: mit
- Created: 2022-04-02T00:37:12.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2024-11-02T13:46:33.000Z (3 months ago)
- Last Synced: 2025-01-22T14:06:23.557Z (10 days ago)
- Topics: clojure, clojurescript, data-flow, response, tenet, unified-response, unifier
- Language: Clojure
- Homepage:
- Size: 169 KB
- Stars: 38
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.adoc
- Changelog: changelog.adoc
- License: license
Awesome Lists containing this project
README
image:https://img.shields.io/github/license/lazy-cat-io/tenet[license,link=license]
image:https://img.shields.io/github/v/release/lazy-cat-io/tenet.svg[https://github.com/lazy-cat-io/tenet/releases]
image:https://img.shields.io/clojars/v/io.lazy-cat/tenet.svg[clojars,link=https://clojars.org/io.lazy-cat/tenet]
image:https://img.shields.io/badge/babashka,%20clojure,%20clojurescript-just_sultanov?style=flat&color=blue&label=%20supports[]image:https://codecov.io/gh/lazy-cat-io/tenet/branch/master/graph/badge.svg?token=BGGNUI43Y2[codecov,https://codecov.io/gh/lazy-cat-io/tenet]
image:https://github.com/lazy-cat-io/tenet/actions/workflows/build.yml/badge.svg[build,https://github.com/lazy-cat-io/tenet/actions/workflows/build.yml]
image:https://github.com/lazy-cat-io/tenet/actions/workflows/deploy.yml/badge.svg[deploy,https://github.com/lazy-cat-io/tenet/actions/workflows/deploy.yml]== io.lazy-cat/tenet
A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow.
=== Rationale
==== Problem statement
Typically, when collaborating on a project, it is essential to establish beforehand the nature of the outcomes to be employed.
Some individuals opt for maps, while others prefer vectors, and still others rely on monads such as `Either`, `Maybe`, and so on.
It is not always evident when a function yields data without any accompanying context, such as `nil`, `42`, and so forth.What does `nil` mean?
It can mean:
- No data
- Something is done or not
- Something went wrongWhat does `42` mean:
- User id?
- Age?Such responses make you think about the current implementation and take time to understand the current context.
Imagine that we have a function that contains some kind of business logic:
[source,clojure]
----
(defn create-user!
[user]
(cond
(not (valid? user)) ??? ;; returns a response that the given data is not valid
(exists? user) ??? ;; returns a response that the email is occupied
:else
(try
(insert! user)
??? ;; returns a response that a new user has been created
(catch SomeDbException _
??? ;; returns a response indicating that
;; there was a problem writing data to the database
))))
----In this case, there are several possible responses that could occur:
- The user's data may not be valid
- The email address may be occupied
- An error may have occurred while writing the data to the database
- Or, finally, a successful response may be returned, such as a user ID or dataAnd how can we add context?
There is a useful data type in Clojure - `qualified (namespaced) keywords`, which can be used to add some context to responses.
- `:user/incorrect`, `:user/exists`
- `:user/created` or `:com.your-company.user/created`With this information, it is clear what happened - we have the context and the data.
Most of the time, we don't write code, we read it, and that's very important.We have added the context, but how should we use it?
Should we use a key-value pair within a map, a vector, a monad, or metadata? And how should we decide which type of response should be classified as an error?We used all the above methods in our practice, and it has always been something inconvenient.
What should be the structure of the map or vector?
Should we create custom object/type and use getters and setters?
This adds problems in further use and looks like OOP.
Should we Use metadata? Unfortunately, metadata cannot be added to some types of data.
And what kind of response is considered an error?==== Solution
This library helps to unify responses.
In short, all the responses are a vector `[ ...]` similar to the hiccup syntax.
E.g. `[:com.your-company.user/created {:user/id 42}]`.There are no requirements for the kind of response and the type of your data.
This library is very small. It is based on only 7 lines of code (2 protocols), and the default implementation is less than 80
lines (without comments and documentation).=== Getting started
Add the following dependency in your project:
.project.clj or build.boot
[source,clojure]
----
[io.lazy-cat/tenet "RELEASE"]
----.deps.edn or bb.edn
[source,clojure]
----
io.lazy-cat/tenet {:mvn/version "RELEASE"}
----=== API
[source,clojure]
----
(ns example
(:require
[tenet.response :as r]
[tenet.response.http :as http]));;;;
;; Defaults
;;;;(r/error? nil) ;; => false
(r/error? 42) ;; => false
(r/error? ::error) ;; => false;; By default, only keyword `:tenet.response/error`, `Throwable` and `js/Error` is considered an error.
;; keyword
(r/error? ::r/error) ;; => true
;; throwable
(r/error? (ex-info "boom!" {})) ;; => true
;; vector using the hiccup syntax
(r/error? [::r/error "Something went wrong"]) ;; => true;;;;
;; Custom errors
;;;;(r/error? :example/error) ;; => false
;; Add a custom error kind to the error registry
(r/derive :example/error) ;; => :example/error
(r/error? :example/error) ;; => true;; Remove a custom error kind from the error registry
(r/underive :example/error) ;; => :example/error;;;;
;; Responses
;;;;(declare valid? explain exists? insert!)
;; In this example, we do not require our library, as we can construct the responses without helpers
(defn create-user!
[user]
(cond
(not (valid? user)) [:user/invalid (explain user)] ;; returns a response that the given data is not valid
(exists? user) [:user/exists user] ;; returns a response that the email is occupied
:else
(try
(let [profile (insert! user)]
[:user/created profile]) ;; returns a response that a new user has been created
(catch Exception e
[:user/not-created e] ;; returns a response indicating that there was a problem writing data to the database
))));; But we have to register our error kinds
(r/derive :user/invalid) ;; => :user/invalid
(r/derive :user/exists) ;; => :user/exists
(r/derive :user/not-created) ;; => :user/not-created(r/error? [:user/exists {:user/id 42}]) ;; => true
(r/kind [:user/exists {:user/id 42}]) ;; => :user/exists;; If necessary, you can change the kind of error to make the correct context
(->> [:db/conflict {:user/id 42}]
(r/as :user/exists)) ;; => [:user/exists {:user/id 42}];;;;
;; Http responses
;;;;;; With a unified approach to response management, we can easily add mappings to HTTP responses
(http/status 42) ;; => 200
(http/status [:user/exists {:user/id 42}]) ;; => 200;; By default,
;; - all unknown non-error response kinds have the status - 200 OK
;; - all error response kinds have the status - 500 Internal Server Error;; But we have to add our custom mappings
(http/derive :user/exists ::http/conflict) ;; => :user/exists
(http/status [:user/exists {:user/id 42}]) ;; => 409;; Namespace `tenet.response.http` contains `wrap-status-middleware' - perhaps this middleware will be useful for you
----=== Performance
See the performance link:src/bench/clojure/perf.clj[tests].
=== License
link:license[Copyright © 2022-2024 lazy-cat.io]