├── .clj-kondo ├── config.edn └── imports │ └── babashka │ └── fs │ └── config.edn ├── .cljfmt.edn ├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .lsp └── config.edn ├── bb.edn ├── build.clj ├── changelog.adoc ├── deps.edn ├── license ├── package-lock.json ├── package.json ├── readme.adoc ├── src ├── bench │ └── clojure │ │ └── perf.clj ├── develop │ └── clojure │ │ ├── example.clj │ │ └── user.clj ├── main │ └── clojure │ │ └── tenet │ │ ├── response.cljc │ │ └── response │ │ ├── http.cljc │ │ └── proto.cljc └── test │ ├── bb │ └── runner.clj │ └── clojure │ └── tenet │ ├── response │ └── http_test.cljc │ └── response_test.cljc ├── tests.edn └── version.tmpl /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:output {:exclude-files []} 2 | 3 | :config-paths [] 4 | 5 | :lint-as {} 6 | 7 | :linters {:consistent-alias 8 | {:aliases {clojure.java.shell shell 9 | clojure.pprint pprint 10 | clojure.set set 11 | clojure.string str}} 12 | 13 | :unresolved-namespace 14 | {:exclude [user criterium.core]} 15 | 16 | :unresolved-symbol 17 | {:exclude [(cljs.test/are [thrown? thrown-with-msg?]) 18 | (cljs.test/is [thrown? thrown-with-msg?]) 19 | (clojure.test/are [thrown? thrown-with-msg?]) 20 | (clojure.test/is [thrown? thrown-with-msg?])]} 21 | 22 | :unsorted-required-namespaces 23 | {:level :warning} 24 | 25 | :unused-referred-var 26 | {:exclude {cljs.test [deftest is testing use-fixtures] 27 | clojure.test [deftest is testing use-fixtures]}}} 28 | 29 | :hooks {}} 30 | -------------------------------------------------------------------------------- /.clj-kondo/imports/babashka/fs/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {babashka.fs/with-temp-dir clojure.core/let}} 2 | -------------------------------------------------------------------------------- /.cljfmt.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | max_line_length = 130 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/**" 9 | - "src/**" 10 | - ".cljfmt.edn" 11 | - "bb.edn" 12 | - "build.clj" 13 | - "deps.edn" 14 | - "package.json" 15 | - "tests.edn" 16 | - "version.tmpl" 17 | pull_request: 18 | branches: 19 | - main 20 | paths: 21 | - ".github/workflows/**" 22 | - "src/**" 23 | - ".cljfmt.edn" 24 | - "bb.edn" 25 | - "build.clj" 26 | - "deps.edn" 27 | - "package.json" 28 | - "tests.edn" 29 | - "version.tmpl" 30 | 31 | jobs: 32 | test: 33 | if: "!contains(github.event.head_commit.message, 'skip ci')" 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | 41 | - name: Setup clojure tools 42 | uses: DeLaGuardo/setup-clojure@12.5 43 | with: 44 | bb: latest 45 | cli: latest 46 | clj-kondo: latest 47 | cljfmt: latest 48 | 49 | - name: Setup openjdk 50 | uses: actions/setup-java@v4 51 | with: 52 | distribution: "temurin" 53 | java-version: "21" 54 | 55 | - name: Setup nodejs 56 | uses: actions/setup-node@v4.1.0 57 | with: 58 | node-version: "22.11.0" 59 | 60 | - name: Setup variables 61 | id: tenet 62 | run: | 63 | TENET_VERSION=$(bb version) 64 | echo "version=${TENET_VERSION}" >> $GITHUB_OUTPUT 65 | 66 | - name: Install deps 67 | run: bb setup 68 | 69 | - name: Cache deps 70 | uses: actions/cache@v4 71 | with: 72 | path: | 73 | ~/.m2/repository 74 | ~/.gitlibs 75 | ~/.clojure 76 | ~/.cpcache 77 | key: ubuntu-deps-${{ hashFiles('deps.edn') }} 78 | restore-keys: ubuntu-deps- 79 | 80 | - name: Run linters 81 | run: bb lint 82 | 83 | - name: Run tests 84 | run: bb test all 85 | 86 | - name: Upload coverage 87 | uses: actions/upload-artifact@v4 88 | with: 89 | path: coverage 90 | name: coverage-${{ steps.tenet.outputs.version }} 91 | 92 | build: 93 | if: "!contains(github.event.head_commit.message, 'skip ci')" 94 | needs: test 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | with: 100 | fetch-depth: 0 101 | 102 | - name: Setup openjdk 103 | uses: actions/setup-java@v4 104 | with: 105 | distribution: "temurin" 106 | java-version: "21" 107 | 108 | - name: Setup clojure tools 109 | uses: DeLaGuardo/setup-clojure@12.5 110 | with: 111 | bb: latest 112 | cli: latest 113 | 114 | - name: Cache deps 115 | uses: actions/cache@v4 116 | with: 117 | path: | 118 | ~/.m2/repository 119 | ~/.gitlibs 120 | ~/.clojure 121 | ~/.cpcache 122 | key: ubuntu-deps-${{ hashFiles('deps.edn') }} 123 | restore-keys: ubuntu-deps- 124 | 125 | - name: Setup variables 126 | id: tenet 127 | run: | 128 | TENET_VERSION=$(bb version) 129 | echo "version=${TENET_VERSION}" >> $GITHUB_OUTPUT 130 | 131 | - name: Build jar 132 | run: bb build jar 133 | 134 | - name: Upload jar 135 | uses: actions/upload-artifact@v4 136 | with: 137 | path: target/tenet.jar 138 | name: tenet-${{ steps.tenet.outputs.version }}.jar 139 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | if: "!contains(github.event.head_commit.message, 'skip ci')" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup clojure tools 19 | uses: DeLaGuardo/setup-clojure@12.5 20 | with: 21 | bb: latest 22 | cli: latest 23 | clj-kondo: latest 24 | cljfmt: latest 25 | 26 | - name: Setup openjdk 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: "temurin" 30 | java-version: "21" 31 | 32 | - name: Setup nodejs 33 | uses: actions/setup-node@v4.1.0 34 | with: 35 | node-version: "22.11.0" 36 | 37 | - name: Setup variables 38 | id: tenet 39 | run: | 40 | TENET_VERSION=$(bb version) 41 | echo "version=${TENET_VERSION}" >> $GITHUB_OUTPUT 42 | 43 | - name: Install deps 44 | run: bb setup 45 | 46 | - name: Cache deps 47 | uses: actions/cache@v4 48 | with: 49 | path: | 50 | ~/.m2/repository 51 | ~/.gitlibs 52 | ~/.clojure 53 | ~/.cpcache 54 | key: ubuntu-deps-${{ hashFiles('deps.edn') }} 55 | restore-keys: ubuntu-deps- 56 | 57 | - name: Run linters 58 | run: bb lint 59 | 60 | - name: Run tests 61 | run: bb test all 62 | 63 | - name: Upload coverage 64 | uses: actions/upload-artifact@v4 65 | with: 66 | path: coverage 67 | name: coverage-${{ steps.tenet.outputs.version }} 68 | 69 | - name: Publish coverage 70 | uses: codecov/codecov-action@v4 71 | with: 72 | token: ${{ secrets.CODECOV_TOKEN }} 73 | files: ./coverage/codecov.json 74 | flags: clojure,clojurescript 75 | os: ubuntu 76 | fail_ci_if_error: false 77 | verbose: true 78 | 79 | publish: 80 | if: "!contains(github.event.head_commit.message, 'skip ci')" 81 | needs: test 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Checkout 85 | uses: actions/checkout@v4 86 | with: 87 | fetch-depth: 0 88 | 89 | - name: Setup openjdk 90 | uses: actions/setup-java@v4 91 | with: 92 | distribution: "temurin" 93 | java-version: "21" 94 | 95 | - name: Setup clojure tools 96 | uses: DeLaGuardo/setup-clojure@12.5 97 | with: 98 | bb: latest 99 | cli: latest 100 | 101 | - name: Cache deps 102 | uses: actions/cache@v4 103 | with: 104 | path: | 105 | ~/.m2/repository 106 | ~/.gitlibs 107 | ~/.clojure 108 | ~/.cpcache 109 | key: ubuntu-deps-${{ hashFiles('deps.edn') }} 110 | restore-keys: ubuntu-deps- 111 | 112 | - name: Setup variables 113 | id: tenet 114 | run: | 115 | TENET_VERSION=$(bb version) 116 | echo "version=${TENET_VERSION}" >> $GITHUB_OUTPUT 117 | echo 'CLOJARS_USERNAME=${{ secrets.CLOJARS_USERNAME }}' >> $GITHUB_ENV; 118 | echo 'CLOJARS_PASSWORD=${{ secrets.CLOJARS_PASSWORD }}' >> $GITHUB_ENV; 119 | 120 | - name: Publish jar 121 | run: bb deploy 122 | 123 | - name: Upload jar 124 | uses: actions/upload-artifact@v4 125 | with: 126 | path: target/tenet.jar 127 | name: tenet-${{ steps.tenet.outputs.version }}.jar 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/.cache 2 | .cljs_node_repl 3 | .cpcache 4 | .lsp/.cache 5 | .idea 6 | coverage 7 | node_modules 8 | out 9 | target 10 | meta.edn 11 | *.iml 12 | -------------------------------------------------------------------------------- /.lsp/config.edn: -------------------------------------------------------------------------------- 1 | {:auto-add-ns-to-new-files? true 2 | :clean {:ns-inner-blocks-indentation :next-line 3 | :sort {:ns true 4 | :require true 5 | :import true 6 | :refer true}} 7 | :cljfmt-config-path ".cljfmt.edn" 8 | :code-lens {:segregate-test-references true} 9 | :dependency-scheme "zipfile" 10 | :keep-parens-when-threading? true 11 | :notify-references-on-file-change true 12 | :semantic-tokens? true} 13 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:min-bb-version "1.12.194" 2 | 3 | :deps {babashka/fs {:mvn/version "0.5.22"}} 4 | 5 | :tasks 6 | {:requires ([babashka.fs :as fs] 7 | [babashka.process :as process] 8 | [clojure.pprint :as pprint] 9 | [clojure.string :as str]) 10 | 11 | :init (do 12 | (def -zone-id (java.time.ZoneId/of "UTC")) 13 | (def -formatter java.time.format.DateTimeFormatter/ISO_OFFSET_DATE_TIME) 14 | (def -timestamp (.format (java.time.ZonedDateTime/now -zone-id) -formatter)) 15 | 16 | (defn execute 17 | [cmd] 18 | (some->> cmd 19 | (process/tokenize) 20 | (process/process) 21 | :out 22 | (slurp) 23 | (str/trim-newline))) 24 | 25 | (defn pretty-print 26 | ([x] 27 | (pretty-print x {})) 28 | ([x {:keys [right-margin] 29 | :or {right-margin 80}}] 30 | (binding [*print-namespace-maps* false 31 | pprint/*print-right-margin* right-margin] 32 | (pprint/pprint x)))) 33 | 34 | (def -organization "lazy-cat-io") 35 | (def -repository "tenet") 36 | (def -lib "io.lazy-cat/tenet") 37 | (def -branch (-> (execute "git rev-parse --abbrev-ref HEAD") (str/lower-case))) 38 | (def -commit (-> (execute "git rev-parse --short HEAD") (str/lower-case))) 39 | (def -git-count-revs (-> (execute "git rev-list HEAD --count"))) 40 | 41 | (def -deployable? 42 | (= "main" -branch)) 43 | 44 | (def -version 45 | (-> "version.tmpl" 46 | (slurp) 47 | (str/trim-newline) 48 | (str/replace "{{ git-count-revs }}" -git-count-revs))) 49 | 50 | (def -meta 51 | {:organization -organization 52 | :repository -repository 53 | :branch -branch 54 | :commit -commit 55 | :version -version 56 | :timestamp -timestamp})) 57 | 58 | :enter (let [{:keys [doc print-doc?] 59 | :or {print-doc? true}} (current-task)] 60 | (when (and print-doc? doc) 61 | (println (format "\n▸ [%s v%s] %s" -lib -version doc)))) 62 | 63 | ;; 64 | ;; Tasks 65 | ;; 66 | 67 | version {:doc "Show version" 68 | :print-doc? false 69 | :override-builtin true 70 | :task (println -version)} 71 | 72 | setup {:doc "Setup dependencies" 73 | :task (shell "npm ci")} 74 | 75 | outdated {:doc "Check for outdated dependencies" 76 | :task (case (some-> *command-line-args* first str/lower-case) 77 | "upgrade" (shell "clojure -M:nop:outdated --main antq.core --upgrade --force") 78 | (shell "clojure -M:nop:outdated --main antq.core"))} 79 | 80 | clean {:doc "Run cleanup" 81 | :task (doseq [dir ["target" "coverage" "out" ".cljs_node_repl"]] 82 | (println (format "Removing %s..." dir)) 83 | (fs/delete-tree dir))} 84 | 85 | lint {:doc "Run linters" 86 | :task (case (some-> *command-line-args* first str/lower-case) 87 | "fix" (shell "cljfmt fix src") 88 | (do 89 | (shell "cljfmt check src") 90 | (shell "clj-kondo --lint src")))} 91 | 92 | repl {:doc "Run nREPL" 93 | :override-builtin true 94 | :depends [clean -build:meta] 95 | :task (shell "clj -M:bench:test:develop --main nrepl.cmdline --interactive --middleware '[cider.nrepl/cider-middleware,cider.piggieback/wrap-cljs-repl]'")} 96 | 97 | -test:bb {:doc "Run babashka tests" 98 | :task (shell "./src/test/bb/runner.clj")} 99 | -test:clj {:doc "Run clojure tests" 100 | :task (shell "clojure -M:nop:test --main kaocha.runner --focus :clojure")} 101 | -test:cljs {:doc "Run clojurescript tests" 102 | :task (shell "clojure -M:nop:test --main kaocha.runner --focus :clojurescript")} 103 | -test:all {:task (do 104 | (shell "clojure -M:nop:test --main kaocha.runner") 105 | (run '-test:bb))} 106 | 107 | test {:doc "Run tests" 108 | :depends [clean] 109 | :task (case (some-> *command-line-args* first str/lower-case) 110 | "all" (run '-test:all) 111 | "bb" (run 'test:bb) 112 | "clj" (run 'test:clj) 113 | "cljs" (run 'test:cljs) 114 | (shell (str/join \space (into ["clojure -M:nop:test --main kaocha.runner"] *command-line-args*))))} 115 | 116 | -build:meta {:doc "Build metadata" 117 | :task (do 118 | (fs/create-dirs "src/main/resources/io/lazy-cat/tenet") 119 | (->> -meta 120 | (pretty-print) 121 | (with-out-str) 122 | (spit "src/main/resources/io/lazy-cat/tenet/meta.edn")))} 123 | 124 | -build:jar {:doc "Build jar" 125 | :depends [clean -build:meta] 126 | :task (shell (format "clojure -T:build jar :version '\"%s\"'" -version))} 127 | 128 | build {:doc "Run build" 129 | :print-doc? false 130 | :task (case (some-> *command-line-args* first str/lower-case) 131 | "meta" (run '-build:meta) 132 | "jar" (run '-build:jar) 133 | (run '-build:jar))} 134 | 135 | install {:doc "Install jar" 136 | :depends [-build:jar] 137 | :task (shell "clojure -T:build install")} 138 | 139 | deploy {:doc "Deploy jar" 140 | :task (if-not -deployable? 141 | (do 142 | (println "Allowed branches: main, develop") 143 | (println (format "Current branch: %s" -branch))) 144 | (do 145 | (run '-build:jar) 146 | (shell "clojure -T:build deploy")))}}} 147 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require 3 | [clojure.tools.build.api :as b] 4 | [deps-deploy.deps-deploy :as d])) 5 | 6 | (def lib 'io.lazy-cat/tenet) 7 | (def class-dir "target/classes") 8 | (def basis (delay (b/create-basis {:project "deps.edn"}))) 9 | (def jar-file "target/tenet.jar") 10 | (def src-dirs ["src/main/clojure" "src/main/resources"]) 11 | 12 | (defn pom-template 13 | [version] 14 | [[:description "A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow."] 15 | [:url "https://github.com/lazy-cat-io/tenet"] 16 | [:licenses 17 | [:license 18 | [:name "MIT License"] 19 | [:url "https://github.com/lazy-cat-io/tenet/blob/main/license"]]] 20 | [:developers 21 | [:developer 22 | [:name "Ilshat Sultanov"]]] 23 | [:scm 24 | [:url "https://github.com/lazy-cat-io/tenet"] 25 | [:connection "scm:git:https://github.com/lazy-cat-io/tenet.git"] 26 | [:developerConnection "scm:git:ssh:git@github.com:lazy-cat-io/tenet.git"] 27 | [:tag (str "v" version)]]]) 28 | 29 | (defn jar 30 | [{:keys [version]}] 31 | (println "Writing pom.xml...") 32 | (b/write-pom {:class-dir class-dir 33 | :lib lib 34 | :version version 35 | :basis @basis 36 | :src-dirs src-dirs 37 | :pom-data (pom-template version)}) 38 | (println "Copying sources...") 39 | (b/copy-dir {:src-dirs src-dirs 40 | :target-dir class-dir}) 41 | (println "Building jar...") 42 | (b/jar {:class-dir class-dir 43 | :jar-file jar-file}) 44 | (println "Done...")) 45 | 46 | (defn install 47 | [_] 48 | (d/deploy {:installer :local 49 | :artifact jar-file 50 | :pom-file (b/pom-path {:lib lib, :class-dir class-dir})})) 51 | 52 | (defn deploy 53 | [_] 54 | (d/deploy {:installer :remote 55 | :artifact jar-file 56 | :pom-file (b/pom-path {:lib lib, :class-dir class-dir})})) 57 | -------------------------------------------------------------------------------- /changelog.adoc: -------------------------------------------------------------------------------- 1 | == Changelog 2 | 3 | === Unreleased 4 | 5 | TBD 6 | 7 | --- 8 | 9 | === Released 10 | 11 | ==== v2.0.101 (2024-11-02) 12 | 13 | **BREAKING CHANGES**: 14 | 15 | - In version 2, the API has undergone a complete redesign in order to improve performance and make the API more user-friendly 16 | - The previous version of the library is stored in the `v1` branch 17 | 18 | ==== v1.0.67 (2022-05-16) 19 | 20 | ===== Fixed 21 | 22 | - After some research, the removal of the helper function `cl-format` was reverted 23 | 24 | ==== v1.0.64 (2022-05-16) 25 | 26 | ===== Added 27 | 28 | - The common response builder - `tenet.response/as` 29 | 30 | ===== Fixed 31 | 32 | - Removed the helper function `cl-format` for the ability to work with GraalVM 33 | 34 | ==== v1.0.57 (2022-04-30) 35 | 36 | Public API is stable. 37 | 38 | ===== Added 39 | 40 | - Some helper functions and macros 41 | 42 | ==== v0.1.20 (2022-04-13) 43 | 44 | The first official release 45 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main/clojure" "src/main/resources"] 2 | 3 | :deps {} 4 | 5 | :aliases {:bench {:extra-paths ["src/bench/clojure" "src/bench/resources"] 6 | :extra-deps {criterium/criterium {:mvn/version "0.4.6"} 7 | com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.4.0"}} 8 | :jvm-opts ["-server" "-Xmx4096m" "-Dclojure.compiler.direct-linking=true"]} 9 | 10 | :develop {:extra-paths ["src/develop/clojure" "src/develop/resources"] 11 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 12 | org.clojure/clojurescript {:mvn/version "1.11.132"} 13 | cider/piggieback {:mvn/version "0.5.3"} 14 | nrepl/nrepl {:mvn/version "1.3.0"} 15 | cider/cider-nrepl {:mvn/version "0.50.2"} 16 | hashp/hashp {:mvn/version "0.2.2"}} 17 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"]} 18 | 19 | :test {:extra-paths ["src/test/clojure" "src/test/resources"] 20 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} 21 | lambdaisland/kaocha-cloverage {:mvn/version "1.1.89"} 22 | com.lambdaisland/kaocha-cljs {:mvn/version "1.5.154"}}} 23 | 24 | :build {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.10.5", :git/sha "2a21b7a"} 25 | slipset/deps-deploy {:mvn/version "0.2.2"}} 26 | :jvm-opts ["-Dclojure.compiler.direct-linking=true" 27 | "-Dclojure.spec.skip-macros=true"] 28 | :ns-default build} 29 | 30 | :nop {:extra-deps {org.slf4j/slf4j-nop {:mvn/version "2.0.16"}}} 31 | 32 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.10.1241"}}}}} 33 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 lazy-cat.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tenet", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "npm-check-updates": "^17.1.9", 9 | "ws": "^8.5.0" 10 | } 11 | }, 12 | "node_modules/npm-check-updates": { 13 | "version": "17.1.9", 14 | "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.9.tgz", 15 | "integrity": "sha512-Gfv5S8NNJKTilM1gesFNYka6bUaBs5LnVyPjApXPQphHijrlLFDMw1uSmwYMZbvJSkLZSOx03e8CHcG0Td5SMA==", 16 | "dev": true, 17 | "bin": { 18 | "ncu": "build/cli.js", 19 | "npm-check-updates": "build/cli.js" 20 | }, 21 | "engines": { 22 | "node": "^18.18.0 || >=20.0.0", 23 | "npm": ">=8.12.1" 24 | } 25 | }, 26 | "node_modules/ws": { 27 | "version": "8.18.0", 28 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 29 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 30 | "dev": true, 31 | "engines": { 32 | "node": ">=10.0.0" 33 | }, 34 | "peerDependencies": { 35 | "bufferutil": "^4.0.1", 36 | "utf-8-validate": ">=5.0.2" 37 | }, 38 | "peerDependenciesMeta": { 39 | "bufferutil": { 40 | "optional": true 41 | }, 42 | "utf-8-validate": { 43 | "optional": true 44 | } 45 | } 46 | } 47 | }, 48 | "dependencies": { 49 | "npm-check-updates": { 50 | "version": "17.1.9", 51 | "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.9.tgz", 52 | "integrity": "sha512-Gfv5S8NNJKTilM1gesFNYka6bUaBs5LnVyPjApXPQphHijrlLFDMw1uSmwYMZbvJSkLZSOx03e8CHcG0Td5SMA==", 53 | "dev": true 54 | }, 55 | "ws": { 56 | "version": "8.18.0", 57 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 58 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 59 | "dev": true, 60 | "requires": {} 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "npm-check-updates": "^17.1.9", 4 | "ws": "^8.5.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /readme.adoc: -------------------------------------------------------------------------------- 1 | image:https://img.shields.io/github/license/lazy-cat-io/tenet[license,link=license] 2 | image:https://img.shields.io/github/v/release/lazy-cat-io/tenet.svg[https://github.com/lazy-cat-io/tenet/releases] 3 | image:https://img.shields.io/clojars/v/io.lazy-cat/tenet.svg[clojars,link=https://clojars.org/io.lazy-cat/tenet] 4 | image:https://img.shields.io/badge/babashka,%20clojure,%20clojurescript-just_sultanov?style=flat&color=blue&label=%20supports[] 5 | 6 | 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] 7 | 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] 8 | 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] 9 | 10 | == io.lazy-cat/tenet 11 | 12 | A Clojure(Script) library, which helps to create explicit and understandable results to unify and simplify the data flow. 13 | 14 | === Rationale 15 | 16 | ==== Problem statement 17 | 18 | Typically, when collaborating on a project, it is essential to establish beforehand the nature of the outcomes to be employed. 19 | Some individuals opt for maps, while others prefer vectors, and still others rely on monads such as `Either`, `Maybe`, and so on. 20 | It is not always evident when a function yields data without any accompanying context, such as `nil`, `42`, and so forth. 21 | 22 | What does `nil` mean? 23 | 24 | It can mean: 25 | 26 | - No data 27 | - Something is done or not 28 | - Something went wrong 29 | 30 | What does `42` mean: 31 | 32 | - User id? 33 | - Age? 34 | 35 | Such responses make you think about the current implementation and take time to understand the current context. 36 | 37 | Imagine that we have a function that contains some kind of business logic: 38 | 39 | [source,clojure] 40 | ---- 41 | (defn create-user! 42 | [user] 43 | (cond 44 | (not (valid? user)) ??? ;; returns a response that the given data is not valid 45 | (exists? user) ??? ;; returns a response that the email is occupied 46 | :else 47 | (try 48 | (insert! user) 49 | ??? ;; returns a response that a new user has been created 50 | (catch SomeDbException _ 51 | ??? ;; returns a response indicating that 52 | ;; there was a problem writing data to the database 53 | )))) 54 | ---- 55 | 56 | In this case, there are several possible responses that could occur: 57 | 58 | - The user's data may not be valid 59 | - The email address may be occupied 60 | - An error may have occurred while writing the data to the database 61 | - Or, finally, a successful response may be returned, such as a user ID or data 62 | 63 | And how can we add context? 64 | 65 | There is a useful data type in Clojure - `qualified (namespaced) keywords`, which can be used to add some context to responses. 66 | 67 | - `:user/incorrect`, `:user/exists` 68 | - `:user/created` or `:com.your-company.user/created` 69 | 70 | With this information, it is clear what happened - we have the context and the data. 71 | Most of the time, we don't write code, we read it, and that's very important. 72 | 73 | We have added the context, but how should we use it? 74 | 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? 75 | 76 | We used all the above methods in our practice, and it has always been something inconvenient. 77 | 78 | What should be the structure of the map or vector? 79 | 80 | Should we create custom object/type and use getters and setters? 81 | This adds problems in further use and looks like OOP. 82 | Should we Use metadata? Unfortunately, metadata cannot be added to some types of data. 83 | And what kind of response is considered an error? 84 | 85 | ==== Solution 86 | 87 | This library helps to unify responses. 88 | 89 | In short, all the responses are a vector `[ ...]` similar to the hiccup syntax. 90 | E.g. `[:com.your-company.user/created {:user/id 42}]`. 91 | 92 | There are no requirements for the kind of response and the type of your data. 93 | 94 | This library is very small. It is based on only 7 lines of code (2 protocols), and the default implementation is less than 80 95 | lines (without comments and documentation). 96 | 97 | === Getting started 98 | 99 | Add the following dependency in your project: 100 | 101 | .project.clj or build.boot 102 | [source,clojure] 103 | ---- 104 | [io.lazy-cat/tenet "RELEASE"] 105 | ---- 106 | 107 | .deps.edn or bb.edn 108 | [source,clojure] 109 | ---- 110 | io.lazy-cat/tenet {:mvn/version "RELEASE"} 111 | ---- 112 | 113 | === API 114 | 115 | 116 | [source,clojure] 117 | ---- 118 | (ns example 119 | (:require 120 | [tenet.response :as r] 121 | [tenet.response.http :as http])) 122 | 123 | ;;;; 124 | ;; Defaults 125 | ;;;; 126 | 127 | (r/error? nil) ;; => false 128 | (r/error? 42) ;; => false 129 | (r/error? ::error) ;; => false 130 | 131 | ;; By default, only keyword `:tenet.response/error`, `Throwable` and `js/Error` is considered an error. 132 | 133 | ;; keyword 134 | (r/error? ::r/error) ;; => true 135 | ;; throwable 136 | (r/error? (ex-info "boom!" {})) ;; => true 137 | ;; vector using the hiccup syntax 138 | (r/error? [::r/error "Something went wrong"]) ;; => true 139 | 140 | ;;;; 141 | ;; Custom errors 142 | ;;;; 143 | 144 | (r/error? :example/error) ;; => false 145 | 146 | ;; Add a custom error kind to the error registry 147 | (r/derive :example/error) ;; => :example/error 148 | (r/error? :example/error) ;; => true 149 | 150 | ;; Remove a custom error kind from the error registry 151 | (r/underive :example/error) ;; => :example/error 152 | 153 | ;;;; 154 | ;; Responses 155 | ;;;; 156 | 157 | (declare valid? explain exists? insert!) 158 | 159 | ;; In this example, we do not require our library, as we can construct the responses without helpers 160 | 161 | (defn create-user! 162 | [user] 163 | (cond 164 | (not (valid? user)) [:user/invalid (explain user)] ;; returns a response that the given data is not valid 165 | (exists? user) [:user/exists user] ;; returns a response that the email is occupied 166 | :else 167 | (try 168 | (let [profile (insert! user)] 169 | [:user/created profile]) ;; returns a response that a new user has been created 170 | (catch Exception e 171 | [:user/not-created e] ;; returns a response indicating that there was a problem writing data to the database 172 | )))) 173 | 174 | ;; But we have to register our error kinds 175 | 176 | (r/derive :user/invalid) ;; => :user/invalid 177 | (r/derive :user/exists) ;; => :user/exists 178 | (r/derive :user/not-created) ;; => :user/not-created 179 | 180 | (r/error? [:user/exists {:user/id 42}]) ;; => true 181 | (r/kind [:user/exists {:user/id 42}]) ;; => :user/exists 182 | 183 | ;; If necessary, you can change the kind of error to make the correct context 184 | (->> [:db/conflict {:user/id 42}] 185 | (r/as :user/exists)) ;; => [:user/exists {:user/id 42}] 186 | 187 | ;;;; 188 | ;; Http responses 189 | ;;;; 190 | 191 | ;; With a unified approach to response management, we can easily add mappings to HTTP responses 192 | 193 | (http/status 42) ;; => 200 194 | (http/status [:user/exists {:user/id 42}]) ;; => 200 195 | 196 | ;; By default, 197 | ;; - all unknown non-error response kinds have the status - 200 OK 198 | ;; - all error response kinds have the status - 500 Internal Server Error 199 | 200 | ;; But we have to add our custom mappings 201 | (http/derive :user/exists ::http/conflict) ;; => :user/exists 202 | (http/status [:user/exists {:user/id 42}]) ;; => 409 203 | 204 | ;; Namespace `tenet.response.http` contains `wrap-status-middleware' - perhaps this middleware will be useful for you 205 | ---- 206 | 207 | === Performance 208 | 209 | See the performance link:src/bench/clojure/perf.clj[tests]. 210 | 211 | === License 212 | 213 | link:license[Copyright © 2022-2024 lazy-cat.io] 214 | -------------------------------------------------------------------------------- /src/bench/clojure/perf.clj: -------------------------------------------------------------------------------- 1 | (ns perf 2 | (:require 3 | [criterium.core :as bench] 4 | [tenet.response :as r] 5 | [tenet.response.http :as http])) 6 | 7 | ;; MacBook Pro (15-inch, 2018/2019) 8 | ;; Intel(R) Core(TM) i7-8850H (12) @ 2.60 GHz 9 | ;; 16.00 GiB 10 | ;; macOS Sequoia 15.0.1 x86_64 11 | 12 | ;;;; 13 | ;; Defaults 14 | ;;;; 15 | 16 | (bench/quick-bench 17 | (r/error? ::r/error)) ; => true 18 | 19 | ; (out) Evaluation count : 20083626 in 6 samples of 3347271 calls. 20 | ; (out) Execution time mean : 14.875154 ns 21 | ; (out) Execution time std-deviation : 0.147461 ns 22 | ; (out) Execution time lower quantile : 14.692583 ns ( 2.5%) 23 | ; (out) Execution time upper quantile : 15.040448 ns (97.5%) 24 | ; (out) Overhead used : 15.049260 ns 25 | 26 | (bench/quick-bench 27 | (r/error? ::not-error)) ; => false 28 | 29 | ; (out) Evaluation count : 19714662 in 6 samples of 3285777 calls. 30 | ; (out) Execution time mean : 15.423519 ns 31 | ; (out) Execution time std-deviation : 0.092422 ns 32 | ; (out) Execution time lower quantile : 15.261812 ns ( 2.5%) 33 | ; (out) Execution time upper quantile : 15.515286 ns (97.5%) 34 | ; (out) Overhead used : 15.049260 ns 35 | 36 | ;;;; 37 | ;; Java objects 38 | ;;;; 39 | 40 | (bench/quick-bench 41 | (r/kind 42)) ; => nil 42 | 43 | ; (out) Evaluation count : 46984380 in 6 samples of 7830730 calls. 44 | ; (out) Execution time mean : 0.432813 ns 45 | ; (out) Execution time std-deviation : 0.038412 ns 46 | ; (out) Execution time lower quantile : 0.391860 ns ( 2.5%) 47 | ; (out) Execution time upper quantile : 0.470783 ns (97.5%) 48 | ; (out) Overhead used : 12.341496 ns 49 | 50 | ;;;; 51 | ;; Vector 52 | ;;;; 53 | 54 | (def user-exists 55 | [:user/exists {:user/id 42}]) 56 | 57 | (bench/quick-bench 58 | (r/kind user-exists)) ; => :user/exists 59 | 60 | ; (out) Evaluation count : 31009008 in 6 samples of 5168168 calls. 61 | ; (out) Execution time mean : 4.626609 ns 62 | ; (out) Execution time std-deviation : 0.274217 ns 63 | ; (out) Execution time lower quantile : 4.281799 ns ( 2.5%) 64 | ; (out) Execution time upper quantile : 4.951825 ns (97.5%) 65 | ; (out) Overhead used : 15.041038 ns 66 | 67 | (r/derive :user/exists) ; => :user/exists 68 | 69 | (bench/quick-bench 70 | (r/error? user-exists)) ; => true 71 | 72 | ; (out) Evaluation count : 14381382 in 6 samples of 2396897 calls. 73 | ; (out) Execution time mean : 26.636206 ns 74 | ; (out) Execution time std-deviation : 0.239904 ns 75 | ; (out) Execution time lower quantile : 26.394512 ns ( 2.5%) 76 | ; (out) Execution time upper quantile : 26.934575 ns (97.5%) 77 | ; (out) Overhead used : 15.050630 ns 78 | 79 | (r/underive :user/exists) ; => :user/exists 80 | 81 | ;;;; 82 | ;; Builders 83 | ;;;; 84 | 85 | (bench/quick-bench 86 | (r/as ::error 42)) ; => [:perf/error 42] 87 | 88 | ; (out) Evaluation count : 28434054 in 6 samples of 4739009 calls. 89 | ; (out) Execution time mean : 13.717179 ns 90 | ; (out) Execution time std-deviation : 10.464559 ns 91 | ; (out) Execution time lower quantile : 6.355319 ns ( 2.5%) 92 | ; (out) Execution time upper quantile : 28.825937 ns (97.5%) 93 | ; (out) Overhead used : 15.041038 ns 94 | 95 | (bench/quick-bench 96 | (r/as ::error user-exists)) ; => [:perf/error {:user/id 42}] 97 | 98 | ; eval (root-form): (bench/quick-bench (r/as ::error user-exists)) 99 | ; (out) Evaluation count : 14432754 in 6 samples of 2405459 calls. 100 | ; (out) Execution time mean : 27.933007 ns 101 | ; (out) Execution time std-deviation : 1.615869 ns 102 | ; (out) Execution time lower quantile : 26.703075 ns ( 2.5%) 103 | ; (out) Execution time upper quantile : 30.424757 ns (97.5%) 104 | ; (out) Overhead used : 15.048625 ns 105 | 106 | ;;;; 107 | ;; Http 108 | ;;;; 109 | 110 | (r/derive :user/exists) ; => :user/exists 111 | (http/derive :user/exists ::http/conflict) ;; :user/exists 112 | (r/error? user-exists) ; => true 113 | 114 | (bench/quick-bench 115 | (http/status user-exists)) ; => 409 116 | 117 | ; (out) Evaluation count : 12864336 in 6 samples of 2144056 calls. 118 | ; (out) Execution time mean : 34.513275 ns 119 | ; (out) Execution time std-deviation : 0.259090 ns 120 | ; (out) Execution time lower quantile : 34.107100 ns ( 2.5%) 121 | ; (out) Execution time upper quantile : 34.782836 ns (97.5%) 122 | ; (out) Overhead used : 12.343262 ns 123 | 124 | (r/underive :user/exists) ; => :user/exists 125 | (http/underive :user/exists) ; => :user/exists 126 | 127 | (r/error? user-exists) ; => false 128 | 129 | (bench/quick-bench 130 | (http/status user-exists)) ; => 200 131 | 132 | ; (out) Evaluation count : 21069486 in 6 samples of 3511581 calls. 133 | ; (out) Execution time mean : 16.189475 ns 134 | ; (out) Execution time std-deviation : 0.041101 ns 135 | ; (out) Execution time lower quantile : 16.130563 ns ( 2.5%) 136 | ; (out) Execution time upper quantile : 16.233606 ns (97.5%) 137 | ; (out) Overhead used : 12.343262 ns 138 | -------------------------------------------------------------------------------- /src/develop/clojure/example.clj: -------------------------------------------------------------------------------- 1 | (ns example 2 | (:require 3 | [tenet.response :as r] 4 | [tenet.response.http :as http])) 5 | 6 | ;;;; 7 | ;; Defaults 8 | ;;;; 9 | 10 | (r/error? nil) ;; => false 11 | (r/error? 42) ;; => false 12 | (r/error? ::error) ;; => false 13 | 14 | ;; By default, only keyword `:tenet.response/error`, `Throwable` and `js/Error` is considered an error. 15 | 16 | ;; keyword 17 | (r/error? ::r/error) ;; => true 18 | ;; throwable 19 | (r/error? (ex-info "boom!" {})) ;; => true 20 | ;; vector using the hiccup syntax 21 | (r/error? [::r/error "Something went wrong"]) ;; => true 22 | 23 | ;;;; 24 | ;; Custom errors 25 | ;;;; 26 | 27 | (r/error? :example/error) ;; => false 28 | 29 | ;; Add a custom error kind to the error registry 30 | (r/derive :example/error) ;; => :example/error 31 | (r/error? :example/error) ;; => true 32 | 33 | ;; Remove a custom error kind from the error registry 34 | (r/underive :example/error) ;; => :example/error 35 | 36 | ;;;; 37 | ;; Responses 38 | ;;;; 39 | 40 | (declare valid? explain exists? insert!) 41 | 42 | ;; In this example, we do not require our library, as we can construct the responses without helpers 43 | 44 | (defn create-user! 45 | [user] 46 | (cond 47 | (not (valid? user)) [:user/invalid (explain user)] ;; returns a response that the given data is not valid 48 | (exists? user) [:user/exists user] ;; returns a response that the email is occupied 49 | :else 50 | (try 51 | (let [profile (insert! user)] 52 | [:user/created profile]) ;; returns a response that a new user has been created 53 | (catch Exception e 54 | [:user/not-created e] ;; returns a response indicating that there was a problem writing data to the database 55 | )))) 56 | 57 | ;; But we have to register our error kinds 58 | 59 | (r/derive :user/invalid) ;; => :user/invalid 60 | (r/derive :user/exists) ;; => :user/exists 61 | (r/derive :user/not-created) ;; => :user/not-created 62 | 63 | (r/error? [:user/exists {:user/id 42}]) ;; => true 64 | (r/kind [:user/exists {:user/id 42}]) ;; => :user/exists 65 | 66 | ;; If necessary, you can change the kind of error to make the correct context 67 | (->> [:db/conflict {:user/id 42}] 68 | (r/as :user/exists)) ;; => [:user/exists {:user/id 42}] 69 | 70 | ;;;; 71 | ;; Http responses 72 | ;;;; 73 | 74 | ;; With a unified approach to response management, we can easily add mappings to HTTP responses 75 | 76 | (http/status 42) ;; => 200 77 | (http/status [:user/exists {:user/id 42}]) ;; => 200 78 | 79 | ;; By default, 80 | ;; - all unknown non-error response kinds have the status - 200 OK 81 | ;; - all error response kinds have the status - 500 Internal Server Error 82 | 83 | ;; But we have to add our custom mappings 84 | (http/derive :user/exists ::http/conflict) ;; => :user/exists 85 | (http/status [:user/exists {:user/id 42}]) ;; => 409 86 | 87 | ;; Namespace `tenet.response.http` contains `wrap-status-middleware' - perhaps this middleware will be useful for you 88 | -------------------------------------------------------------------------------- /src/develop/clojure/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Development helper functions." 3 | (:require 4 | [cider.piggieback :as pb] 5 | [cljs.repl.node :as rn] 6 | [hashp.core])) 7 | 8 | (defn cljs-repl 9 | "Starts a ClojureScript REPL." 10 | [] 11 | (pb/cljs-repl (rn/repl-env))) 12 | -------------------------------------------------------------------------------- /src/main/clojure/tenet/response.cljc: -------------------------------------------------------------------------------- 1 | (ns tenet.response 2 | (:refer-clojure :exclude [derive underive]) 3 | (:require 4 | [tenet.response.proto :as r]) 5 | #?(:clj 6 | (:import 7 | (clojure.lang 8 | IPersistentSet 9 | Keyword 10 | PersistentList 11 | PersistentVector)))) 12 | 13 | ;;;; 14 | ;; Defaults 15 | ;;;; 16 | 17 | (extend-type nil 18 | r/Builder 19 | (as [_ kind] [kind nil]) 20 | 21 | r/Response 22 | (kind [_])) 23 | 24 | (extend-type #?(:clj Object :cljs default) 25 | r/Builder 26 | (as [obj kind] [kind obj]) 27 | 28 | r/Response 29 | (kind [_])) 30 | 31 | (extend-type #?(:clj Throwable :cljs js/Error) 32 | r/Builder 33 | (as [e kind] [kind e]) 34 | 35 | r/Response 36 | (kind [_] ::error)) 37 | 38 | (extend-type #?(:clj Keyword :cljs cljs.core/Keyword) 39 | r/Builder 40 | (as [k kind] [kind k]) 41 | 42 | r/Response 43 | (kind [k] k)) 44 | 45 | #?(:clj 46 | (extend-type PersistentList 47 | r/Builder 48 | (as [xs kind] (into [kind] (rest xs))) 49 | 50 | r/Response 51 | (kind [xs] 52 | #?(:bb (first xs) 53 | :clj (.first xs))))) 54 | 55 | (extend-type #?(:clj PersistentVector :cljs cljs.core/PersistentVector) 56 | r/Builder 57 | (as [xs kind] 58 | #?(:bb (assoc xs 0 kind) 59 | :clj (.assocN xs 0 kind) 60 | :cljs (-assoc-n xs 0 kind))) 61 | 62 | r/Response 63 | (kind [xs] 64 | #?(:bb (nth xs 0) 65 | :clj (.nth xs 0) 66 | :cljs (-nth xs 0)))) 67 | 68 | ;;;; 69 | ;; Registry 70 | ;;;; 71 | 72 | (defonce errors 73 | #{::error}) 74 | 75 | (defn derive 76 | [kind] 77 | #?(:clj (alter-var-root #'errors conj kind) 78 | :cljs (set! errors (conj errors kind))) 79 | kind) 80 | 81 | (defn underive 82 | [kind] 83 | #?(:clj (alter-var-root #'errors disj kind) 84 | :cljs (set! errors (disj errors kind))) 85 | kind) 86 | 87 | ;;;; 88 | ;; Response 89 | ;;;; 90 | 91 | (defn error? 92 | [x] 93 | #?(:bb (contains? errors (r/kind x)) 94 | :clj (.contains ^IPersistentSet errors (r/kind x)) 95 | :cljs (boolean (-lookup ^cljs.core/ILookup errors (r/kind x))))) 96 | 97 | (defn kind 98 | [x] 99 | (r/kind x)) 100 | 101 | (defn as 102 | [kind x] 103 | (r/as x kind)) 104 | -------------------------------------------------------------------------------- /src/main/clojure/tenet/response/http.cljc: -------------------------------------------------------------------------------- 1 | (ns tenet.response.http 2 | (:refer-clojure :exclude [derive underive]) 3 | (:require 4 | [tenet.response] 5 | [tenet.response.proto :as r]) 6 | #?(:clj 7 | (:import 8 | (clojure.lang 9 | IPersistentMap)))) 10 | 11 | ;;;; 12 | ;; Defaults 13 | ;;;; 14 | 15 | (defonce statuses 16 | {nil 200 ;; default status if the response kind is not derived 17 | ::continue 100 18 | ::switching-protocols 101 19 | ::processing 102 20 | ::early-hints 103 21 | ::ok 200 22 | ::created 201 23 | ::accepted 202 24 | ::non-authoritative-information 203 25 | ::no-content 204 26 | ::reset-content 205 27 | ::partial-content 206 28 | ::multi-status 207 29 | ::already-reported 208 30 | ::im-used 226 31 | ::multiple-choice 300 32 | ::moved-permanently 301 33 | ::found 302 34 | ::see-other 303 35 | ::not-modified 304 36 | ::use-proxy 305 37 | ::switch-proxy 306 ;; unused - this response code is no longer used, it is just reserved. It was used in a previous version of the HTTP/1.1 specification. 38 | ::temporary-redirect 307 39 | ::permanent-redirect 308 40 | ::bad-request 400 41 | ::unauthorized 401 42 | ::payment-required 402 43 | ::forbidden 403 44 | ::not-found 404 45 | ::method-not-allowed 405 46 | ::not-acceptable 406 47 | ::proxy-authentication-required 407 48 | ::request-timeout 408 49 | ::conflict 409 50 | ::gone 410 51 | ::length-required 411 52 | ::precondition-failed 412 53 | ::payload-too-large 413 54 | ::uri-too-long 414 55 | ::unsupported-media-type 415 56 | ::range-not-satisfiable 416 57 | ::expectation-failed 417 58 | ::im-a-teapot 418 59 | ::misdirected-request 421 60 | ::unprocessable-entity 422 61 | ::locked 423 62 | ::failed-dependency 424 63 | ::too-early 425 64 | ::upgrade-required 426 65 | ::precondition-required 428 66 | ::too-many-requests 429 67 | ::request-header-fields-too-large 431 68 | ::unavailable-for-legal-reasons 451 69 | ::internal-server-error 500 70 | ::not-implemented 501 71 | ::bad-gateway 502 72 | ::service-unavailable 503 73 | ::gateway-timeout 504 74 | ::http-version-not-supported 505 75 | ::variant-also-negotiates 506 76 | ::insufficient-storage 507 77 | ::loop-detected 508 78 | ::not-extended 510 79 | ::network-authentication-required 511}) 80 | 81 | ;;;; 82 | ;; Registry 83 | ;;;; 84 | 85 | (defonce mappings 86 | {nil ::ok 87 | :tenet.response/error ::internal-server-error}) 88 | 89 | (defn derive 90 | [kind parent] 91 | #?(:clj (alter-var-root #'mappings assoc kind parent) 92 | :cljs (set! mappings (assoc mappings kind parent))) 93 | kind) 94 | 95 | (defn underive 96 | [kind] 97 | #?(:clj (alter-var-root #'mappings dissoc kind) 98 | :cljs (set! mappings (dissoc mappings kind))) 99 | kind) 100 | 101 | ;;;; 102 | ;; Response 103 | ;;;; 104 | 105 | (defn status 106 | [x] 107 | #?(:bb (->> (r/kind x) 108 | (get mappings) 109 | (get statuses)) 110 | :clj (->> (r/kind x) 111 | (.valAt ^IPersistentMap mappings) 112 | (.valAt ^IPersistentMap statuses)) 113 | :cljs (->> (r/kind x) 114 | (-lookup ^cljs.core/ILookup mappings) 115 | (-lookup ^cljs.core/ILookup statuses)))) 116 | 117 | ;;;; 118 | ;; Middleware 119 | ;;;; 120 | 121 | (defn response->status 122 | [res] 123 | (assoc res :status (status (:body res)))) 124 | 125 | (defn wrap-status-middleware 126 | [handler] 127 | (fn 128 | ([req] 129 | (response->status (handler req))) 130 | ([req respond raise] 131 | (handler req (fn [res] (respond (response->status res))) raise)))) 132 | -------------------------------------------------------------------------------- /src/main/clojure/tenet/response/proto.cljc: -------------------------------------------------------------------------------- 1 | (ns tenet.response.proto) 2 | 3 | (defprotocol Response 4 | (kind [this])) 5 | 6 | (defprotocol Builder 7 | (as [this kind])) 8 | -------------------------------------------------------------------------------- /src/test/bb/runner.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require 4 | '[babashka.classpath :as cp] 5 | '[clojure.test :as t]) 6 | 7 | (cp/add-classpath "src/main/clojure:src/test/clojure") 8 | 9 | (require 10 | 'tenet.response-test 11 | 'tenet.response.http-test) 12 | 13 | (def test-results 14 | (t/run-tests 'tenet.response-test 'tenet.response.http-test)) 15 | 16 | (let [{:keys [fail error]} test-results] 17 | (when (pos? (+ fail error)) 18 | (System/exit 1))) 19 | -------------------------------------------------------------------------------- /src/test/clojure/tenet/response/http_test.cljc: -------------------------------------------------------------------------------- 1 | (ns tenet.response.http-test 2 | (:require 3 | #?@(:clj [[clojure.test :refer [deftest testing is]]] 4 | :cljs [[cljs.test :refer [deftest testing is]]]) 5 | [tenet.response.http :as sut])) 6 | 7 | (def handler 8 | #(sut/wrap-status-middleware (constantly %))) 9 | 10 | (def async-handler 11 | #(sut/wrap-status-middleware 12 | (fn [_req respond _raise] 13 | (respond %)))) 14 | 15 | (deftest wrap-status-middleware-test 16 | (testing "sync" 17 | (testing "plain objects as an unified response" 18 | (is (= {:status 200, :body 42} 19 | ((handler {:status 500, :body 42}) 20 | {})))) 21 | 22 | (testing "vector as an unified response" 23 | (let [before {:status 200, :body [::created 42]} 24 | after {:status 201, :body [::created 42]}] 25 | (is (= before 26 | ((handler {:status 500, :body [::created 42]}) {}))) 27 | (sut/derive ::created ::sut/created) 28 | (is (= after 29 | ((handler {:status 500, :body [::created 42]}) {}))) 30 | (sut/underive ::created) 31 | (is (= before 32 | ((handler {:status 500, :body [::created 42]}) {})))))) 33 | 34 | (testing "async" 35 | (testing "plain objects as an unified response" 36 | (is (= {:status 200, :body 42} 37 | ((async-handler {:status 200, :body 42}) {} identity nil)))) 38 | 39 | (testing "vector as an unified response" 40 | (let [before {:status 200, :body [::created 42]} 41 | after {:status 201, :body [::created 42]}] 42 | (is (= before 43 | ((async-handler {:status 500, :body [::created 42]}) {} identity nil))) 44 | (sut/derive ::created ::sut/created) 45 | (is (= after 46 | ((async-handler {:status 500, :body [::created 42]}) {} identity nil))) 47 | (sut/underive ::created) 48 | (is (= before 49 | ((async-handler {:status 500, :body [::created 42]}) {} identity nil))))))) 50 | -------------------------------------------------------------------------------- /src/test/clojure/tenet/response_test.cljc: -------------------------------------------------------------------------------- 1 | (ns tenet.response-test 2 | (:require 3 | #?@(:clj [[clojure.test :refer [deftest testing is]]] 4 | :cljs [[cljs.test :refer [deftest testing is]]]) 5 | [tenet.response :as sut])) 6 | 7 | (deftest error?-test 8 | (is (not (sut/error? nil))) 9 | (is (not (sut/error? 42))) 10 | (is (not (sut/error? "42"))) 11 | (is (sut/error? ::sut/error)) 12 | 13 | (is (not (sut/error? ::error))) 14 | (sut/derive ::error) 15 | (is (sut/error? ::error)) 16 | (sut/underive ::error) 17 | (is (not (sut/error? ::error)))) 18 | 19 | (deftest kind-test 20 | (let [e #?(:clj (Exception. "boom!") 21 | :cljs (js/Error. "boom!"))] 22 | (is (nil? (sut/kind nil))) 23 | (is (nil? (sut/kind 42))) 24 | (is (nil? (sut/kind "42"))) 25 | (is (= ::sut/error (sut/kind e))) 26 | (is (= ::created (sut/kind [::created 42]))) 27 | #?(:clj (is (= ::created (sut/kind '(::created 42))))) 28 | 29 | ;; this is the expected behavior 30 | (is (= 1 (sut/kind [1 2 3]))) 31 | #?(:clj (is (= 1 (sut/kind '(1 2 3))))))) 32 | 33 | (deftest as-test 34 | (let [e #?(:clj (Exception. "boom!") 35 | :cljs (js/Error. "boom!"))] 36 | (is (= [::error nil] (sut/as ::error nil))) 37 | (is (= [::error 42] (sut/as ::error 42))) 38 | (is (= [::error "42"] (sut/as ::error "42"))) 39 | (is (= [::error ::kw] (sut/as ::error ::kw))) 40 | (is (= [::error e] (sut/as ::error e))) 41 | (is (= [::error 42] (sut/as ::error [::created 42]))) 42 | (is (= [::error [1 2 3]] (sut/as ::error [::created [1 2 3]]))) 43 | #?(:clj (is (= [::error 42] (sut/as ::error '(::created 42))))) 44 | #?(:clj (is (= [::error [1 2 3]] (sut/as ::error '(::created [1 2 3]))))) 45 | 46 | ;; this is the expected behavior 47 | (is (= [::error 2 3] (sut/as ::error [1 2 3]))) 48 | #?(:clj (is (= [::error 2 3] (sut/as ::error '(1 2 3))))) 49 | 50 | (is (= [::created 42] 51 | (->> 42 52 | (sut/as ::error) 53 | (sut/as ::created)))))) 54 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:kaocha/fail-fast? false 3 | :kaocha/color? true 4 | :kaocha/reporter [kaocha.report/documentation] 5 | 6 | :kaocha.plugin.randomize/randomize? true 7 | 8 | :capture-output? true 9 | 10 | :plugins [:kaocha.plugin/capture-output 11 | :kaocha.plugin/cloverage 12 | :kaocha.plugin/filter 13 | :kaocha.plugin/hooks 14 | :kaocha.plugin/print-invocations 15 | :kaocha.plugin/randomize 16 | :kaocha.plugin.alpha/info] 17 | 18 | :tests [{:id :clojure 19 | :source-paths ["src/main/clojure"] 20 | :test-paths ["src/test/clojure"]} 21 | {:id :clojurescript 22 | :type :kaocha.type/cljs 23 | :source-paths ["src/main/clojure"] 24 | :test-paths ["src/test/clojure"]}] 25 | 26 | :cloverage/opts {:output "coverage" 27 | :low-watermark 50 28 | :high-watermark 80 29 | :summary? true 30 | :html? true 31 | :codecov? true}} 32 | -------------------------------------------------------------------------------- /version.tmpl: -------------------------------------------------------------------------------- 1 | 2.0.{{ git-count-revs }}-SNAPSHOT 2 | --------------------------------------------------------------------------------