diff --git a/.github/workflows/graal-tests.yml b/.github/workflows/graal-tests.yml index b2ea89b..14495f4 100644 --- a/.github/workflows/graal-tests.yml +++ b/.github/workflows/graal-tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: version: 'latest' @@ -18,12 +18,12 @@ jobs: components: 'native-image' github-token: ${{ secrets.GITHUB_TOKEN }} - - uses: DeLaGuardo/setup-clojure@10.0 + - uses: DeLaGuardo/setup-clojure@12.5 with: lein: latest bb: latest - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.m2/repository key: deps-${{ hashFiles('deps.edn') }} diff --git a/.github/workflows/main-tests.yml b/.github/workflows/main-tests.yml index 900a96e..faa64d6 100644 --- a/.github/workflows/main-tests.yml +++ b/.github/workflows/main-tests.yml @@ -10,17 +10,17 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'corretto' java-version: ${{ matrix.java }} - - uses: DeLaGuardo/setup-clojure@10.0 + - uses: DeLaGuardo/setup-clojure@12.5 with: lein: latest - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache-deps with: path: ~/.m2/repository diff --git a/.gitignore b/.gitignore index 5f96acc..7cc5b29 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pom.xml* /target/ /checkouts/ /logs/ +/wiki/.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 80eec79..ec0e2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ This is a minor **feature release**, and should be a non-breaking upgrade. ``` > This is a **feature release**. Should be non-breaking. -> See [here](https://github.com/ptaoussanis/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. +> See [here](https://github.com/taoensso/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. ### Since `1.8.0` @@ -62,7 +62,7 @@ This is a minor **feature release**, and should be a non-breaking upgrade. ``` > This is a **maintenance release**. _Should_ be non-breaking. -> See [here](https://github.com/ptaoussanis/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. +> See [here](https://github.com/taoensso/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. ### Since `v1.7.2` @@ -77,7 +77,7 @@ This is a minor **feature release**, and should be a non-breaking upgrade. ``` > This is a **maintenance release**. Changes may be BREAKING for some users, see relevant commits referenced below for details. -> See [here](https://github.com/ptaoussanis/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. +> See [here](https://github.com/taoensso/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. ### Changes since `v1.6.0` @@ -94,7 +94,7 @@ This is a minor **feature release**, and should be a non-breaking upgrade. ``` > Minor feature release. _Should_ be non-breaking. -> See [here](https://github.com/ptaoussanis/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. +> See [here](https://github.com/taoensso/encore#recommended-steps-after-any-significant-dependency-update) for a tip re: general recommended steps when updating any Clojure/Script dependencies. Identical to `1.6.0-RC1`. diff --git a/README.md b/README.md index b344307..e651d0a 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,17 @@ # Truss -#### Assertions micro-library for Clojure/Script +### Assertions micro-library for Clojure/Script **Truss** is a tiny Clojure/Script library that provides fast and flexible **runtime assertions** with **terrific error messages**. Use it as a complement or alternative to [clojure.spec](https://clojure.org/about/spec), [core.typed](https://github.com/clojure/core.typed), etc. -Egyptian ship with rope truss, the oldest known use of trusses (about 1250 BC). +Egyptian ship with rope truss, the oldest known use of trusses (about 1250 BC). > A doubtful friend is worse than a certain enemy. Let a man be one thing or the other, and we then know how to meet him. - Aesop ## Latest release/s -- `2023-07-31` `1.11.0`: [changes](../../releases/tag/v1.11.0) +- `2023-07-31` `1.11.0`: [release info](../../releases/tag/v1.11.0) [![Main tests][Main tests SVG]][Main tests URL] [![Graal tests][Graal tests SVG]][Graal tests URL] @@ -30,6 +30,14 @@ See [here][GitHub releases] for earlier releases. - No commitment or costly buy-in: use it just when+where needed - Perfect for library authors: no bulky dependencies +## Video demo + +See for intro and usage: + + + Truss demo video + + ## Quickstart 1\. Add the [relevant dependency](#latest-releases) to your project: @@ -42,7 +50,7 @@ deps.edn: com.taoensso/truss {:mvn/version "x-y-z"} 2\. Setup your namespace imports: ```clojure -(ns my-ns (:require [taoensso.truss :as truss :refer (have have! have?)])) +(ns my-ns (:require [taoensso.truss :as truss :refer [have have?]])) ``` 3\. Truss uses the simple `(predicate arg)` pattern familiar to Clojure users: @@ -73,27 +81,16 @@ That's everything most users will need to know, but see the [documentation](#doc ## Documentation -- [Full documentation][GitHub wiki] (detailed usage, etc.) -- Auto-generated API reference: [Codox][Codox docs], [clj-doc][clj-doc docs] - -## Motivation - - - - - -See [here][GitHub wiki] for more. +- [Wiki][GitHub wiki] (getting started, usage, etc.) +- API reference: [Codox][Codox docs], [clj-doc][clj-doc docs] ## Funding -You can [help support continued work][funding] on this project, thank you!! ๐Ÿ™ - -Copyright © 2015-2023 [Peter Taoussanis][]. -Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). +You can [help support][sponsor] continued work on this project, thank you!! ๐Ÿ™ ## License -Copyright © 2014-2023 [Peter Taoussanis][]. +Copyright © 2014-2024 [Peter Taoussanis][]. Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). @@ -103,7 +100,7 @@ Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). [GitHub wiki]: ../../wiki [Peter Taoussanis]: https://www.taoensso.com -[funding]: https://www.taoensso.com/clojure/backers +[sponsor]: https://www.taoensso.com/sponsor diff --git a/bb/graal_tests.clj b/bb/graal_tests.clj index b8c00d6..3397ebe 100755 --- a/bb/graal_tests.clj +++ b/bb/graal_tests.clj @@ -28,7 +28,9 @@ (let [graalvm-home (System/getenv "GRAALVM_HOME") bin-dir (str (fs/file graalvm-home "bin"))] (shell (executable bin-dir "gu") "install" "native-image") - (shell (executable bin-dir "native-image") "-jar" "target/graal-tests.jar" "--no-fallback" "graal_tests"))) + (shell (executable bin-dir "native-image") + "--features=clj_easy.graal_build_time.InitClojureClasses" + "--no-fallback" "-jar" "target/graal-tests.jar" "graal_tests"))) (defn run-tests [] (let [{:keys [out]} (shell {:out :string} (executable "." "graal_tests"))] diff --git a/project.clj b/project.clj index 2b0d84d..70b5406 100644 --- a/project.clj +++ b/project.clj @@ -7,8 +7,8 @@ {:name "Eclipse Public License - v 1.0" :url "https://www.eclipse.org/legal/epl-v10.html"} - :dependencies - [] + :test-paths ["test" #_"src"] + :dependencies [] :profiles {;; :default [:base :system :user :provided :dev] @@ -17,8 +17,18 @@ :c1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]} :c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]} :c1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} - :test - {:jvm-opts ["-Dtaoensso.elide-deprecated=true"] + + :graal-tests + {:source-paths ["test"] + :main taoensso.graal-tests + :aot [taoensso.graal-tests] + :uberjar-name "graal-tests.jar" + :dependencies + [[org.clojure/clojure "1.11.1"] + [com.github.clj-easy/graal-build-time "1.0.5"]]} + + :dev + {:jvm-opts ["-server" "-Dtaoensso.elide-deprecated=true"] :global-vars {*warn-on-reflection* true *assert* true @@ -27,29 +37,17 @@ :dependencies [[org.clojure/test.check "1.1.1"] [com.taoensso/encore "3.77.0" - :exclusions [com.taoensso/truss]]]} + :exclusions [com.taoensso/truss]]] - :graal-tests - {:dependencies [[org.clojure/clojure "1.11.1"] - [com.github.clj-easy/graal-build-time "0.1.4"]] - :main taoensso.graal-tests - :aot [taoensso.graal-tests] - :uberjar-name "graal-tests.jar"} + :plugins + [[lein-pprint "1.3.2"] + [lein-ancient "0.7.0"] + [lein-cljsbuild "1.1.8"] + [com.taoensso.forks/lein-codox "0.10.10"]] - :dev - [:c1.11 :test - {:jvm-opts ["-server"] - :plugins - [[lein-pprint "1.3.2"] - [lein-ancient "0.7.0"] - [lein-cljsbuild "1.1.8"] - [com.taoensso.forks/lein-codox "0.10.10"]] - - :codox - {:language #{:clojure :clojurescript} - :base-language :clojure}}]} - - :test-paths ["test" #_"src"] + :codox + {:language #{:clojure :clojurescript} + :base-language :clojure}}} :cljsbuild {:test-commands {"node" ["node" "target/test.js"]} diff --git a/talk.jpg b/talk.jpg deleted file mode 100644 index c6b98fb..0000000 Binary files a/talk.jpg and /dev/null differ diff --git a/wiki/.gitignore b/wiki/.gitignore new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/wiki/.gitignore @@ -0,0 +1 @@ +README.md diff --git a/wiki/1-Getting-started.md b/wiki/1-Getting-started.md new file mode 100644 index 0000000..86db25c --- /dev/null +++ b/wiki/1-Getting-started.md @@ -0,0 +1,329 @@ +# Setup + +Add the [relevant dependency](../#latest-releases) to your project: + +```clojure +Leiningen: [com.taoensso/truss "x-y-z"] ; or +deps.edn: com.taoensso/truss {:mvn/version "x-y-z"} +``` + +And setup your namespace imports: + +```clojure +(ns my-ns (:require [taoensso.truss :as truss :refer [have have?]])) +``` + +# Basics + +The main way to use Truss is with the [`have`](https://taoensso.github.io/truss/taoensso.truss.html#var-have) macro. + +You give it a predicate, and an argument that you believe should satisfy the predicate. + +For example: + +```clojure +(defn greet + "Given a string username, prints a greeting message." + [username] + (println "hello" (have string? username))) +``` + +In this case the predicate is `string?` and argument is `username`: + +- If `(string? username)` is truthy: the invariant **succeeds** and `(have ...)` returns the given username. +- If `(string? username)` is falsey: the invariant **fails** and a detailed **error is thrown** to help you debug. + +That's the basic idea. + +These `(have )` annotations are standard Clojure forms that both **documents the intention of the code** in a way that **cannot go stale**, and provides a **runtime check** that throws a detailed error message on any unexpected violation. + +Everything else documented here is either: + +- Advice on how best to use Truss, or +- Details on features for convenience or advanced situations + +## When to use Truss assertions + +You use Truss to **formalize assumptions** that you have about your data (e.g. **function arguments**, **intermediate values**, or **current application state** at some point in your execution flow). + +So any time you find yourself making **implementation choices based on implicit information** (e.g. the state your application should be in if this code is running) - that's when you might want to reach for Truss instead of a comment or Clojure assertion. + +Use Truss assertions like **salt in good cooking**; a little can go a long way. + +## `have` variants + +While most users will only need to use the base `have` macro, a few variations are provided for convenience: + +Macro | On success | On failure | Subject to elision? | Comment +:--- | :--- | :--- | :--- | :--- +[have](https://taoensso.github.io/truss/taoensso.truss.html#var-have) | Returns given arg/s | Throws | Yes | Most common +[have!](https://taoensso.github.io/truss/taoensso.truss.html#var-have.21) | Returns given arg/s | Throws | No | As above, without elision +[have?](https://taoensso.github.io/truss/taoensso.truss.html#var-have.3F) | Returns true | Throws | Yes | Useful in pre/post conditions +[have!?](https://taoensso.github.io/truss/taoensso.truss.html#var-have.21.3F) | Returns true | Throws | No | As above, without elision + +In all cases: + +- The basic syntax is identical +- The behaviour on failure is identical + +What varies is the return value, and whether elision is possible. + +# Examples + +> All examples are from [`/examples/truss_examples.cljc`](../blob/master/examples/truss_examples.cljc) + +Truss's sweet spot is often in longer, complex code (difficult to show here). So these examples are mostly examples of **syntax**, not **use case**. In particular, they mostly focus on simple **argument type assertions** since those are the easiest to understand. + +In practice, you'll often find more value from assertions about your **application state** or **intermediate `let` values** _within_ a larger piece of code. + +## Inline assertions and bindings + +A Truss `(have )` form will either throw or return the given argument. This lets you use these forms within other expressions and within `let` bindings, etc. + +```clojure +;; You can add an assertion inline +(println (have string? "foo")) + +;; Or you can add an assertion to your bindings +(let [s (have string? "foo")] + (println s)) + +;; Anything that fails the predicate will throw an error +(have string? 42) ; => +;; Invariant failed at truss-examples[41,1]: (string? 42) +;; {:dt #inst "2023-07-31T09:58:07.927-00:00", +;; :pred clojure.core/string?, +;; :arg {:form 42, :value 42, :type java.lang.Long}, +;; :env {:elidable? true, :*assert* true}, +;; :loc +;; {:ns truss-examples, +;; :line 41, +;; :column 1, +;; :file "examples/truss_examples.cljc"}} + +;; Truss also automatically traps and handles exceptions +(have string? (/ 1 0)) ; => +;; Invariant failed at truss-examples[54,1]: (string? (/ 1 0)) +;; +;; Error evaluating arg: Divide by zero +;; {:dt #inst "2023-07-31T09:59:06.149-00:00", +;; :pred clojure.core/string?, +;; :arg +;; {:form (/ 1 0), +;; :value truss/undefined-arg, +;; :type truss/undefined-arg}, +;; :env {:elidable? true, :*assert* true}, +;; :loc +;; {:ns truss-examples, +;; :line 54, +;; :column 1, +;; :file "examples/truss_examples.cljc"}, +;; :err +;; #error +;; {:cause "Divide by zero" +;; :via +;; [{:type java.lang.ArithmeticException +;; :message "Divide by zero" +;; :at [clojure.lang.Numbers divide "Numbers.java" 190]}] +;; :trace +;; [<...>]}} +``` + +## Destructured bindings + +```clojure +;; You can assert against multipe args at once +(let [[x y z] (have string? "foo" "bar" "baz")] + (str x y z)) ; => "foobarbaz" + +;; This won't compromise error message clarity +(let [[x y z] (have string? "foo" 42 "baz")] + (str x y z)) ; => +;; Invariant failed at truss-examples[89,15]: (string? 42) +;; {:dt #inst "2023-07-31T10:01:00.991-00:00", +;; :pred clojure.core/string?, +;; :arg {:form 42, :value 42, :type java.lang.Long}, +;; :env {:elidable? true, :*assert* true}, +;; :loc +;; {:ns truss-examples, +;; :line 89, +;; :column 15, +;; :file "examples/truss_examples.cljc"}} +``` + +## Attaching debug data + +You can attach arbitrary debug data to be displayed on violations: + +```clojure +(defn my-handler [ring-req x y] + (let [[x y] (have integer? x y :data {:ring-req ring-req})] + (* x y))) + +(my-handler {:foo :bar} 5 nil) ; => +;; Invariant failed at truss-examples[107,15]: (integer? y) +;; {:dt #inst "2023-07-31T10:02:03.415-00:00", +;; :pred clojure.core/integer?, +;; :arg {:form y, :value nil, :type nil}, +;; :env {:elidable? true, :*assert* true}, +;; :loc +;; {:ns truss-examples, +;; :line 107, +;; :column 15, +;; :file "examples/truss_examples.cljc"}, +;; :data {:dynamic nil, :arg {:ring-req {:foo :bar}}}} +``` + +## Attaching dynamic debug data + +And you can attach shared debug data at the `binding` level: + +```clojure +(defn wrap-ring-dynamic-assertion-data + "Returns Ring handler wrapped so that assertion violation errors in handler + will include `(data-fn )` as debug data." + [data-fn ring-handler-fn] + (fn [ring-req] + (truss/with-data (data-fn ring-req) + (ring-handler-fn ring-req)))) + +(defn ring-handler [ring-req] + (have? string? 42) ; Will always fail + {:status 200 :body "Done"}) + +(def wrapped-ring-handler + (wrap-ring-dynamic-assertion-data + ;; Include Ring session with all handler's assertion errors: + (fn data-fn [ring-req] {:ring-session (:session ring-req)}) + ring-handler)) + +(wrapped-ring-handler + {:method :get :uri "/" :session {:user-name "Stu"}}) ; => +;; Invariant failed at truss-examples[136,3]: (string? 42) +;; {:dt #inst "2023-07-31T10:02:41.459-00:00", +;; :pred clojure.core/string?, +;; :arg {:form 42, :value 42, :type java.lang.Long}, +;; :env {:elidable? true, :*assert* true}, +;; :loc +;; {:ns truss-examples, +;; :line 136, +;; :column 3, +;; :file "examples/truss_examples.cljc"}, +;; :data {:dynamic {:ring-session {:user-name "Stu"}}, :arg nil}} +``` + +## Assertions within data structures + +```clojure +;;; Compare +(have vector? [:a :b :c]) ; => [:a :b :c] +(have keyword? :in [:a :b :c]) ; => [:a :b :c] +``` + +## Assertions within :pre/:post conditions + +Just make sure to use the `have?` variant which always returns a truthy val on success: + +```clojure +(defn square [n] + ;; Note the use of `have?` instead of `have` + {:pre [(have? #(or (nil? %) (integer? %)) n)] + :post [(have? integer? %)]} + (let [n (or n 1)] + (* n n))) + +(square 5) ; => 25 +(square nil) ; => 1 +``` + +## Special predicates + +Truss offers some shorthands for your convenience. **These are all optional**: the same effect can always be achieved with an equivalent predicate fn: + +```clojure +;; A predicate can be anything +(have #(and (integer? %) (odd? %) (> % 5)) 7) ; => 7 + +;; Omit the predicate as a shorthand for #(not (nil? %)) +(have "foo") ; => "foo" +(have nil) ; => Error + +;;; There's a number of other optional shorthands + +;; Combine predicates (or) +(have [:or nil? string?] "foo") ; => "foo" + +;; Combine predicates (and) +(have [:and integer? even? pos?] 6) ; => 6 + +;; Element of (checks for set containment) +(have [:el #{:a :b :c :d nil}] :b) ; => :b +(have [:el #{:a :b :c :d nil}] nil) ; => nil +(have [:el #{:a :b :c :d nil}] :e) ; => Error + +;; Superset +(have [:set>= #{:a :b}] #{:a :b :c}) ; => #{:a :b :c} + +;; Key superset +(have [:ks>= #{:a :b}] {:a "A" :b nil :c "C"}) ; => {:a "A" :b nil :c "C"} + +;; Non-nil keys +(have [:ks-nnil? #{:a :b}] {:a "A" :b nil :c "C"}) ; => Error +``` + +## Complex validators + +No need for any special syntax or concepts, just define a function as you'd like: + +```clojure +;; A custom predicate: +(defn pos-int? [x] (and (integer? x) (pos? x))) + +(defn have-person + "Returns given arg if it's a valid `person`, otherwise throws an error" + [person] + (truss/with-data {:person person} ; (Optional) setup some extra debug data + (have? map? person) + (have? [:ks>= #{:age :name}] person) + (have? [:or nil? pos-int?] (:age person))) + person ; Return input if nothing's thrown + ) + +(have-person {:name "Steve" :age 33}) ; => {:name "Steve", :age 33} +(have-person {:name "Alice" :age "33"}) ; => Error +``` + +# Motivation + + + +Clojure is a beautiful language full of smart trade-offs that tends to produce production code that's short, simple, and easy to understand. + +But every language necessarily has trade-offs. In the case of Clojure, **dynamic typing** leads to one of the more common challenges that I've observed in the wild: **debugging or refactoring large codebases**. + +Specifically: + + * **Undocumented type assumptions** changing (used to be this thing was never nil; now it can be) + * Documented **type assumptions going stale** (forgot to update comments) + * **Unhelpful error messages** when a type assumption is inevitably violated (it crashed in production? why?) + +Thankfully, this list is almost exhaustive; in my experience these few causes often account for **80%+ of real-world incidental difficulty**. + +So **Truss** targets these issues with a **practical 80% solution** that emphasizes: + + 1. **Ease of adoption** (incl. partial/precision/gradual adoption) + 2. **Ease of use** (non-invasive API, trivial composition, etc.) + 3. **Flexibility** (scales well to large, complex systems) + 4. **Speed** (blazing fast => can use in production, in speed-critical code) + 5. **Simplicity** (lean API, zero dependencies, tiny codebase) + +The first is particularly important since the need for assertions in a good Clojure codebase is surprisingly _rare_. + +Every codebase has trivial parts and complex parts. Parts that suffer a lot of churn, and parts that haven't changed in years. Mission-critical parts (bank transaction backend), and those that aren't so mission-critical (prototype UI for the marketing department). + +Having the freedom to reinforce code only **where and when you judge it worthwhile**: + + 1. Let's you (/ your developers) easily evaluate the lib + 2. Makes it more likely that you (/ your developers) will actually _use_ the lib + 3. Eliminates upfront buy-in costs + 4. Allows you to retain control over long-term cost/benefit trade-offs diff --git a/wiki/2-FAQ.md b/wiki/2-FAQ.md new file mode 100644 index 0000000..357c81b --- /dev/null +++ b/wiki/2-FAQ.md @@ -0,0 +1,87 @@ +# How to report/log violations? + +By default, Truss just throws an **exception** on any invariant violations. + +You can adjust that behaviour with the [`set-error-fn!`](https://taoensso.github.io/truss/taoensso.truss.html#var-set-error-fn.21) and [`with-error-fn`](https://taoensso.github.io/truss/taoensso.truss.html#var-with-error-fn) utils. + +Some common usage ideas: + +- Use `with-error-fn` to capture violations during unit testing +- Use `set-error-fn!` to _log_ violations with something like [Timbre](https://www.taoensso.com/timbre) + +# Should I annotate my whole API? + +**Please don't**! I'd encourage you to think of Truss assertions like **salt in good cooking**; a little can go a long way, and the need for too much salt can be a sign that something's gone wrong in the cooking. + +Another useful analogy would be the Clojure STM. Good Clojure code tends to use the STM very rarely. When you want the STM, you _really_ want it - but many new Clojure developers end up surprised at just how rarely they end up wanting it in an idiomatic Clojure codebase. + +Do the interns keep getting that argument wrong despite attempts at making the code as clear as possible? By all means, add an assertion. + +More than anything, I tend to use Truss assertions as a form of documentation in long/hairy or critical bits of code to remind myself of any unusual input/output contracts/expectations. E.g. for performance reasons, we _need_ this to be a vector; throw if a list comes in since it means that some consumer has a bug. + +# What's the performance cost? + +Usually insignificant. Truss has been **highly tuned** to minimize both code expansion size[1] and runtime costs. + +In many common cases, a Truss expression expands to no more than `(if (pred arg) arg (throw-detailed-assertion-error!))`. + +```clojure +(quick-bench 1e5 + (if (string? "foo") "foo" (throw (Exception. "Assertion failure"))) + (have string? "foo")) +;; => [4.19 4.17] ; ~4.2ms / 100k iterations +``` + +> [1] This can be important for ClojureScript codebases + +So we're seeing zero overhead against a simple predicate test in this example. In practice this means that predicate costs dominate. + +For simple predicates (including `instance?` checks), modern JITs work great; the runtime performance impact is almost always completely insignificant even in tight loops. + +In rare cases where the cost does matter (e.g. for an unusually expensive predicate), Truss supports complete elision in production code. + +# How to elide Truss checks? + +Disable `clojure.core/*assert*` before macro expansion, and Truss forms will noop. They'll pass their arguments through with **zero performance overhead**. + +If you use Leiningen, an easy way to do this is to add the following to your `project.clj`: + +```clojure +:global-vars {*assert* false} +``` + +# How to prevent elision? + +An extra macro is provided (`have!`) which ignores `*assert*` and so can never be elided. This is handy for implementing (and documenting) critical checks like security assertions that you never want disabled. + +```clojure +(defn get-restricted-resource [ring-session] + ;; This is an important security check so we'll use `have!` here instead of + ;; `have` to make sure the check is never elided (skipped): + (have! string? (:auth-token ring-session)) + + "return-restricted-resource-content") +``` + +# How does Truss compare to alternatives? + +There are several good choices when it comes to providing type and/or structural information to Clojure/Script code, including. [clojure.spec](https://clojure.org/about/spec), [core.typed](https://github.com/clojure/core.typed), [@plumatic/schema](https://github.com/plumatic/schema), [@marick/structural-typing](https://github.com/marick/structural-typing), etc. + +How these compare is a tough question to answer briefly since these projects may have different objectives, and sometimes offer very different trade-offs. + +Some of the variables to consider might include: + +- **Cost of getting started** - e.g. is it cheap/easy to cover an initial/small subset of code? +- **Ease of learning** - e.g. how complex is the syntax/API for newcomers? +- **Flexibility at scale** - e.g. likelihood of encountering frustrating limitations? +- **Performance** - e.g. impact on testing/development/production runtimes? + +To make a useful comparison, ultimately one might want some kind of `relevant-power รท relevant-cost`, relative to some specific context and objectives. + +For my part, I'm really pleased with the balance of particular trade-offs that Truss offers. + +As of 2023, Truss continues to be my preferred/default choice for a wide variety of common cases in projects large and small. + +The best general recommendation I can make is to try actually experiment with the options that seem appealing to you. Nothing beats hands-on experience for deciding what best fits your particular needs and tastes. + +See [here](../wiki#motivation) for some of the specific objectives I had with Truss. \ No newline at end of file diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..af3d591 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,8 @@ +See the menu to the right for content ๐Ÿ‘‰ + +# Contributions welcome + +**PRs very welcome** to help improve this documentation! +See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files. + +\- [Peter Taoussanis](https://www.taoensso.com) \ No newline at end of file diff --git a/wiki/README.md b/wiki/README.md new file mode 100644 index 0000000..e788516 --- /dev/null +++ b/wiki/README.md @@ -0,0 +1,5 @@ +# Attention! + +This wiki is designed for viewing from [here](../../../wiki)! + +Viewing from GitHub's file browser will result in **broken links**. \ No newline at end of file