{"id":13760379,"url":"https://github.com/lazy-cat-io/tenet","last_synced_at":"2025-04-09T11:11:16.675Z","repository":{"id":40298518,"uuid":"476906692","full_name":"lazy-cat-io/tenet","owner":"lazy-cat-io","description":"A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow","archived":false,"fork":false,"pushed_at":"2024-11-02T13:46:33.000Z","size":173,"stargazers_count":38,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-02T09:08:24.028Z","etag":null,"topics":["clojure","clojurescript","data-flow","response","tenet","unified-response","unifier"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lazy-cat-io.png","metadata":{"files":{"readme":"readme.adoc","changelog":"changelog.adoc","contributing":null,"funding":null,"license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-02T00:37:12.000Z","updated_at":"2024-11-02T13:46:33.000Z","dependencies_parsed_at":"2024-12-25T01:11:38.047Z","dependency_job_id":"13760515-6599-44f5-95bd-0eade78b7c16","html_url":"https://github.com/lazy-cat-io/tenet","commit_stats":{"total_commits":67,"total_committers":1,"mean_commits":67.0,"dds":0.0,"last_synced_commit":"9e83678206ddacb2ed8206a542534de26132faad"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazy-cat-io%2Ftenet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazy-cat-io%2Ftenet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazy-cat-io%2Ftenet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazy-cat-io%2Ftenet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lazy-cat-io","download_url":"https://codeload.github.com/lazy-cat-io/tenet/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248027407,"owners_count":21035594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojurescript","data-flow","response","tenet","unified-response","unifier"],"created_at":"2024-08-03T13:01:09.035Z","updated_at":"2025-04-09T11:11:16.656Z","avatar_url":"https://github.com/lazy-cat-io.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"image:https://img.shields.io/github/license/lazy-cat-io/tenet[license,link=license]\nimage:https://img.shields.io/github/v/release/lazy-cat-io/tenet.svg[https://github.com/lazy-cat-io/tenet/releases]\nimage:https://img.shields.io/clojars/v/io.lazy-cat/tenet.svg[clojars,link=https://clojars.org/io.lazy-cat/tenet]\nimage:https://img.shields.io/badge/babashka,%20clojure,%20clojurescript-just_sultanov?style=flat\u0026color=blue\u0026label=%20supports[]\n\nimage:https://codecov.io/gh/lazy-cat-io/tenet/branch/master/graph/badge.svg?token=BGGNUI43Y2[codecov,https://codecov.io/gh/lazy-cat-io/tenet]\nimage: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]\nimage: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]\n\n== io.lazy-cat/tenet\n\nA Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow.\n\n=== Rationale\n\n==== Problem statement\n\nTypically, when collaborating on a project, it is essential to establish beforehand the nature of the outcomes to be employed.\nSome individuals opt for maps, while others prefer vectors, and still others rely on monads such as `Either`, `Maybe`, and so on.\nIt is not always evident when a function yields data without any accompanying context, such as `nil`, `42`, and so forth.\n\nWhat does `nil` mean?\n\nIt can mean:\n\n- No data\n- Something is done or not\n- Something went wrong\n\nWhat does `42` mean:\n\n- User id?\n- Age?\n\nSuch responses make you think about the current implementation and take time to understand the current context.\n\nImagine that we have a function that contains some kind of business logic:\n\n[source,clojure]\n----\n(defn create-user!\n  [user]\n  (cond\n    (not (valid? user)) ??? ;; returns a response that the given data is not valid\n    (exists? user) ??? ;; returns a response that the email is occupied\n    :else\n    (try\n      (insert! user)\n      ??? ;; returns a response that a new user has been created\n      (catch SomeDbException _\n        ??? ;; returns a response indicating that\n            ;; there was a problem writing data to the database\n        ))))\n----\n\nIn this case, there are several possible responses that could occur:\n\n- The user's data may not be valid\n- The email address may be occupied\n- An error may have occurred while writing the data to the database\n- Or, finally, a successful response may be returned, such as a user ID or data\n\nAnd how can we add context?\n\nThere is a useful data type in Clojure - `qualified (namespaced) keywords`, which can be used to add some context to responses.\n\n- `:user/incorrect`, `:user/exists`\n- `:user/created` or `:com.your-company.user/created`\n\nWith this information, it is clear what happened - we have the context and the data.\nMost of the time, we don't write code, we read it, and that's very important.\n\nWe have added the context, but how should we use it?\nShould 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?\n\nWe used all the above methods in our practice, and it has always been something inconvenient.\n\nWhat should be the structure of the map or vector?\n\nShould we create custom object/type and use getters and setters?\nThis adds problems in further use and looks like OOP.\nShould we Use metadata? Unfortunately, metadata cannot be added to some types of data.\nAnd what kind of response is considered an error?\n\n==== Solution\n\nThis library helps to unify responses.\n\nIn short, all the responses are a vector `[\u003ckind\u003e \u003cany data\u003e ...]` similar to the hiccup syntax.\nE.g. `[:com.your-company.user/created {:user/id 42}]`.\n\nThere are no requirements for the kind of response and the type of your data.\n\nThis library is very small. It is based on only 7 lines of code (2 protocols), and the default implementation is less than 80\nlines (without comments and documentation).\n\n=== Getting started\n\nAdd the following dependency in your project:\n\n.project.clj or build.boot\n[source,clojure]\n----\n[io.lazy-cat/tenet \"RELEASE\"]\n----\n\n.deps.edn or bb.edn\n[source,clojure]\n----\nio.lazy-cat/tenet {:mvn/version \"RELEASE\"}\n----\n\n=== API\n\n\n[source,clojure]\n----\n(ns example\n  (:require\n   [tenet.response :as r]\n   [tenet.response.http :as http]))\n\n;;;;\n;; Defaults\n;;;;\n\n(r/error? nil) ;; =\u003e false\n(r/error? 42) ;; =\u003e false\n(r/error? ::error) ;; =\u003e false\n\n;; By default, only keyword `:tenet.response/error`, `Throwable` and `js/Error` is considered an error.\n\n;; keyword\n(r/error? ::r/error) ;; =\u003e true\n;; throwable\n(r/error? (ex-info \"boom!\" {})) ;; =\u003e true\n;; vector using the hiccup syntax\n(r/error? [::r/error \"Something went wrong\"]) ;; =\u003e true\n\n;;;;\n;; Custom errors\n;;;;\n\n(r/error? :example/error) ;; =\u003e false\n\n;; Add a custom error kind to the error registry\n(r/derive :example/error) ;; =\u003e :example/error\n(r/error? :example/error) ;; =\u003e true\n\n;; Remove a custom error kind from the error registry\n(r/underive :example/error) ;; =\u003e :example/error\n\n;;;;\n;; Responses\n;;;;\n\n(declare valid? explain exists? insert!)\n\n;; In this example, we do not require our library, as we can construct the responses without helpers\n\n(defn create-user!\n  [user]\n  (cond\n    (not (valid? user)) [:user/invalid (explain user)] ;; returns a response that the given data is not valid\n    (exists? user) [:user/exists user] ;; returns a response that the email is occupied\n    :else\n    (try\n      (let [profile (insert! user)]\n        [:user/created profile]) ;; returns a response that a new user has been created\n      (catch Exception e\n        [:user/not-created e] ;; returns a response indicating that there was a problem writing data to the database\n        ))))\n\n;; But we have to register our error kinds\n\n(r/derive :user/invalid) ;; =\u003e :user/invalid\n(r/derive :user/exists) ;; =\u003e :user/exists\n(r/derive :user/not-created) ;; =\u003e :user/not-created\n\n(r/error? [:user/exists {:user/id 42}]) ;; =\u003e true\n(r/kind [:user/exists {:user/id 42}]) ;; =\u003e :user/exists\n\n;; If necessary, you can change the kind of error to make the correct context\n(-\u003e\u003e [:db/conflict {:user/id 42}]\n     (r/as :user/exists)) ;; =\u003e [:user/exists {:user/id 42}]\n\n;;;;\n;; Http responses\n;;;;\n\n;; With a unified approach to response management, we can easily add mappings to HTTP responses\n\n(http/status 42) ;; =\u003e 200\n(http/status [:user/exists {:user/id 42}]) ;; =\u003e 200\n\n;; By default,\n;;   - all unknown non-error response kinds have the status - 200 OK\n;;   - all error response kinds have the status - 500 Internal Server Error\n\n;; But we have to add our custom mappings\n(http/derive :user/exists ::http/conflict) ;; =\u003e :user/exists\n(http/status [:user/exists {:user/id 42}]) ;; =\u003e 409\n\n;; Namespace `tenet.response.http` contains `wrap-status-middleware' - perhaps this middleware will be useful for you\n----\n\n=== Performance\n\nSee the performance link:src/bench/clojure/perf.clj[tests].\n\n=== License\n\nlink:license[Copyright © 2022-2024 lazy-cat.io]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flazy-cat-io%2Ftenet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flazy-cat-io%2Ftenet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flazy-cat-io%2Ftenet/lists"}