From 09cd702d7fc76415a39952c69e87e83f666e0baa Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 31 Jul 2023 10:15:26 +0200 Subject: [PATCH] [nop] Update README template --- FUNDING.yml | 2 +- LICENSE => LICENSE.txt | 0 README.md | 467 +++++------------------------------------ 3 files changed, 58 insertions(+), 411 deletions(-) rename LICENSE => LICENSE.txt (100%) diff --git a/FUNDING.yml b/FUNDING.yml index dc3d4d2..964e36a 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,2 +1,2 @@ github: ptaoussanis -custom: "https://www.taoensso.com/clojure/backers" +custom: "https://www.taoensso.com/clojure" diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index 9bc4a65..fc4b05c 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,59 @@ - -Taoensso open-source +Taoensso open source +[**Documentation**](#documentation) | [Latest releases](#latest-releases) | [Get support][GitHub issues] -**[CHANGELOG][]** | [API][] | current [Break Version][]: +# Truss -```clojure -[com.taoensso/truss "1.10.1"] ; See CHANGELOG for details -``` - -> See [here](https://taoensso.com/clojure/backers) if you're interested in helping support my open-source work, thanks! - Peter Taoussanis - -# Truss: great Clojure/Script error messages where you need them most - -**Or**: A **lightweight** alternative to **static typing**, [clojure.spec], [core.typed], [@plumatic/schema], etc. - -**Or**: `(have set? x) => (if (set? x) x (throw-detailed-assertion-error!))` - -**Truss** is a **micro library** for Clojure/Script that provides fast, flexible **runtime condition assertions** with **great error messages**. It can be used to get many of the most important benefits of **static/gradual typing** without the usual rigidity or onboarding costs. - -![Hero][] - -> 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** +#### Assertions micro-library for Clojure/Script -## Features +**Truss** is a tiny Clojure/Script library that provides fast and flexible **runtime assertions** with **terrific error messages**. It can complement or be an alternative to [clojure.spec](https://clojure.org/about/spec), [core.typed](https://github.com/clojure/core.typed), etc. - * **Tiny** cross-platform codebase with **zero external dependencies**. - * Trivial to understand and use. - * Use **just** when+where you need it (**incl. libraries**). - * Minimal (or zero) runtime performance cost. - * A practical **80% solution** (focus on **improving error messages**). +Egyptian ship with rope truss, the oldest known use of trusses (about 1250 BC). -## How does it compare to alternatives? +> 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 -There are several good choices when it comes to providing type and/or structural information to Clojure/Script code, including. [clojure.spec][], [core.typed][], [@plumatic/schema][], [@marick/structural-typing][], etc. +## Latest release/s -How these compare is a tough question to answer briefly since these projects may have different objectives, and sometimes offer very different trade-offs. +- `2023-07-15` `1.10.1`: [changes](../../releases/tag/v1.10.1) -Some of the variables to consider might include: +[![Main tests][Main tests SVG]][Main tests URL] +[![Graal tests][Graal tests SVG]][Graal tests URL] -- **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? +See [here][GitHub releases] for earlier releases. -To make a useful comparison, ultimately one might want some kind of `relevant-power รท relevant-cost`, relative to some specific context and objectives. +## Why Truss? -For my part, I'm really pleased with the balance of particular trade-offs that Truss offers. As of 2022, it 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](https://github.com/ptaoussanis/truss#motivation) for a discussion of my own objectives/priorities with Truss. +- **Tiny** cross-platform Clj/s codebase with **zero dependencies** +- **Trivially easy** to learn, use, and understand +- **Terrific error messages** for quick+easy debugging +- **Terrific performance**: miniscule (!) runtime cost +- Easy **elision** for *zero* runtime cost +- No commitment or costly buy-in: use it just when+where needed +- Perfect for library authors: no bulky transitive dependencies ## Quickstart -Add the necessary dependency to your project: +1\. Add the [relevant dependency](#latest-releases) to your project: ```clojure -Leiningen: [com.taoensso/truss "1.10.1"] ; or -deps.edn: com.taoensso/truss {:mvn/version "1.10.1"} +Leiningen: [com.taoensso/truss "x-y-z"] ; or +deps.edn: com.taoensso/truss {:mvn/version "x-y-z"} ``` -And setup your namespace imports: +2\. Setup your namespace imports: ```clojure -(ns my-clj-ns ; Clojure namespace - (:require [taoensso.truss :as truss :refer (have have! have?)])) - -(ns my-cljs-ns ; ClojureScript namespace - (:require [taoensso.truss :as truss :refer-macros (have have! have?)])) +(ns my-ns (:require [taoensso.truss :as truss :refer (have have! have?)])) ``` -Truss uses a simple `(predicate arg)` pattern that should **immediately feel familiar** to Clojure users: +3\. Truss uses the simple `(predicate arg)` pattern familiar to Clojure users: ```clojure (defn square [n] - (let [n (have integer? n)] ; <- A Truss assertion [1] + (let [n (have integer? n)] ; <- A Truss assertion (* n n))) -;; [1] This basically expands to (if (integer? n) n (throw-detailed-assertion-error!)) +;; This assertion basically expands to: +;; (if (integer? n) n (throw-detailed-assertion-error!)) (square 5) ; => 25 (square nil) ; => @@ -92,382 +69,52 @@ Truss uses a simple `(predicate arg)` pattern that should **immediately feel fam ;; :file "examples/truss_examples.cljc"}} ``` -#### And that's it, you know the Truss API. - -The `(have )` annotation is a standard Clojure form 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. +That's most of what you need to know to use Truss. +Or see the [documentation](#documentation) for more info. -### When to use a Truss assertion +## Documentation -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. +- [Full documentation][GitHub wiki] (**getting started** and more) +- Auto-generated API reference: [Codox][Codox docs], [clj-doc][clj-doc docs] ## Motivation -> Feel free to skim/skip this section :-) - -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 - - - -## Examples! - -> All examples are from [`/examples/truss_examples.cljc`](/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: +See the [full documentation][GitHub wiki] for more info. -```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] -``` +## Funding -### 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 -``` +You can [help support continued work][funding] on this project, thank you!! ๐Ÿ™ -### Special predicates +Copyright © 2012-2023 [Peter Taoussanis][]. +Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). -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 -``` - -### Writing custom 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 -``` - -## FAQ - -#### How can I report/log violations? - -By default, Truss just throws an **exception** on any invariant violations. You can adjust that behaviour with the `set-error-fn!` and `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][] - -#### 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. - -I very rarely use Truss for library code, though I wouldn't hesitate to in cases that might inherently be confusing or to guard against common error cases that'd otherwise be hard to debug. - -#### 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. Disable `clojure.core/*assert*` and Truss forms will noop, passing their arguments through with **zero performance overhead**. - -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") -``` - -> **Tip**: when in doubt just use `have!` instead of `have` - -#### How do I disable `clojure.core/*assert*`? +## License -If you're using Leiningen, you can add the following to your `project.clj`: +Copyright © 2014-2023 [Peter Taoussanis][]. +Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). -```clojure -:global-vars {*assert* false} -``` + -## Contacting me / contributions +[GitHub releases]: ../../releases +[GitHub issues]: ../../issues +[GitHub wiki]: ../../wiki -Please use the project's [GitHub issues page][] for all questions, ideas, etc. **Pull requests welcome**. See the project's [GitHub contributors page][] for a list of contributors. +[Peter Taoussanis]: https://www.taoensso.com +[funding]: https://www.taoensso.com/clojure/backers -Otherwise, you can reach me at [Taoensso.com][]. Happy hacking! + -\- [Peter Taoussanis][Taoensso.com] +[Codox docs]: https://taoensso.github.io/truss/ +[clj-doc docs]: https://cljdoc.org/d/com.taoensso/truss/ -## License +[Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/truss.svg +[Clojars URL]: https://clojars.org/com.taoensso/truss -Distributed under the [EPL v1.0][] \(same as Clojure). -Copyright © 2015-2022 [Peter Taoussanis][Taoensso.com]. - - -[Taoensso.com]: https://www.taoensso.com -[Break Version]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md -[backers]: https://taoensso.com/clojure/backers - - -[CHANGELOG]: https://github.com/ptaoussanis/truss/releases -[API]: http://ptaoussanis.github.io/truss/ -[GitHub issues page]: https://github.com/ptaoussanis/truss/issues -[GitHub contributors page]: https://github.com/ptaoussanis/truss/graphs/contributors -[EPL v1.0]: https://raw.githubusercontent.com/ptaoussanis/truss/master/LICENSE -[Hero]: https://raw.githubusercontent.com/ptaoussanis/truss/master/hero.png "Egyptian ship with rope truss, the oldest known use of trusses (about 1250 BC)" - - -[core.typed]: https://github.com/clojure/core.typed -[clojure.spec]: http://clojure.org/about/spec -[@plumatic/schema]: https://github.com/plumatic/schema -[@marick/structural-typing]: https://github.com/marick/structural-typing/ -[Midje]: https://github.com/marick/Midje -[challenges]: #challenges -[Timbre]: https://github.com/ptaoussanis/timbre +[Main tests SVG]: https://github.com/taoensso/truss/actions/workflows/main-tests.yml/badge.svg +[Main tests URL]: https://github.com/taoensso/truss/actions/workflows/main-tests.yml +[Graal tests SVG]: https://github.com/taoensso/truss/actions/workflows/graal-tests.yml/badge.svg +[Graal tests URL]: https://github.com/taoensso/truss/actions/workflows/graal-tests.yml \ No newline at end of file