-
-
Notifications
You must be signed in to change notification settings - Fork 14
/
truss.cljc
184 lines (144 loc) · 6.3 KB
/
truss.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
(ns taoensso.truss
"An opinionated assertions API for Clojure/Script."
{:author "Peter Taoussanis (@ptaoussanis)"}
(:require [taoensso.truss.impl :as impl :refer [-invariant]]))
(comment (require '[taoensso.encore :as enc]))
;;;; Core API
#?(:clj
(defmacro have
"Takes a pred and one or more vals. Tests pred against each val,
trapping errors. If any pred test fails, throws a detailed assertion error.
Otherwise returns input val/vals for convenient inline-use/binding.
Respects *assert* value so tests can be elided from production for zero
runtime costs.
Provides a small, simple, flexible feature subset to alternative tools like
clojure.spec, core.typed, prismatic/schema, etc.
;; Will throw a detailed error message on invariant violation:
(fn my-fn [x] (str/trim (have string? x)))
You may attach arbitrary debug info to assertion violations like:
`(have string? x :data {:my-arbitrary-debug-info \"foo\"})`
Re: use of Truss assertions within other macro bodies:
Due to CLJ-865, call site information (e.g. line number) of
outer macro will unfortunately be lost.
See `keep-callsite` util for a workaround.
See also `have?`, `have!`."
{:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])}
[& args] `(-invariant :elidable nil ~(:line (meta &form)) ~args)))
#?(:clj
(defmacro have?
"Like `have` but returns `true` on successful tests. In particular, this
can be handy for use with :pre/:post conditions. Compare:
(fn my-fn [x] {:post [(have nil? %)]} nil) ; {:post [nil]} FAILS
(fn my-fn [x] {:post [(have? nil? %)]} nil) ; {:post [true]} passes as intended"
{:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])}
[& args] `(-invariant :elidable :truthy ~(:line (meta &form)) ~args)))
#?(:clj
(defmacro have!
"Like `have` but ignores *assert* value (so can never be elided). Useful
for important conditions in production (e.g. security checks)."
{:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])}
[& args] `(-invariant nil nil ~(:line (meta &form)) ~args)))
#?(:clj
(defmacro have!?
"Specialized cross between `have?` and `have!`. Not used often but can be
handy for semantic clarification and/or to improve multi-val performance
when the return vals aren't necessary.
**WARNING**: Do NOT use in :pre/:post conds since those are ALWAYS subject
to *assert*, directly contradicting the intention of the bang (`!`) here."
{:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])}
[& args] `(-invariant :assertion :truthy ~(:line (meta &form)) ~args)))
(comment :see-tests)
(comment
(macroexpand '(have a))
(macroexpand '(have? [:or nil? string?] "hello"))
(enc/qb 1e5
(with-error-fn nil (have? string? 5))
(with-error-fn (fn [_] :truss/error) (have? string? 5)))
(have string? (range 1000)))
(comment
;; HotSpot is great with these:
(enc/qb 1e4
(string? "a")
(have? "a")
(have string? "a" "b" "c")
(have? [:or nil? string?] "a" "b" "c")
(have? [:or nil? string?] "a" "b" "c" :data "foo"))
;; [ 5.59 26.48 45.82 ] ; 1st gen (macro form)
;; [ 3.31 13.48 36.22 ] ; 2nd gen (fn form)
;; [0.82 1.75 7.57 27.05 ] ; 3rd gen (lean macro form)
;; [0.4 0.47 1.3 1.77 1.53] ; 4th gen (macro preds)
(enc/qb 1e4
(have string? :in ["foo" "bar" "baz"])
(have? string? :in ["foo" "bar" "baz"]))
(macroexpand '(have string? 5))
(macroexpand '(have string? 5 :data "foo"))
(macroexpand '(have string? 5 :data (enc/get-env)))
(let [x :x] (have string? 5 :data (enc/get-env)))
(have string? 5)
(have string? 5 :data {:a "a"})
(have string? 5 :data {:a (/ 5 0)})
((fn [x]
(let [a "a" b "b"]
(have string? x :data {:env (enc/get-env)}))) 5)
(do
(set! *assert* false)
(have? integer? 4.0))
;; Combinations: truthy?, single?, in? (8 combinations)
(do (def i1 1) (def v1 [1 2 3]) (def s1 #{1 2 3}))
(macroexpand '(have? integer? 1))
(macroexpand '(have? integer? 1 2 i1))
(macroexpand '(have? integer? :in [1 2 i1]))
(macroexpand '(have? integer? :in [1 2] [3 4 i1] v1))
(macroexpand '(have integer? 1))
(macroexpand '(have integer? 1 2 i1))
(macroexpand '(have integer? :in [1 2 i1]))
(macroexpand '(have integer? :in [1 2] [3 4 i1] v1))
(have? integer? :in s1)
(have integer? :in s1)
(have integer? :in #{1 2 3})
(have integer? :in #{1 2 3} [4 5 6] #{7 8 9} s1))
;;;; Utils
#?(:clj
(defmacro keep-callsite
"CLJ-865 unfortunately means that it's currently not possible
for an inner macro to access the &form metadata of an outer macro.
This means that inner macros lose call site information like the
line number of the outer macro.
This util offers a workaround to authors of the outer macro:
(defmacro foo1 [x] `(truss/have ~x)) ; W/o line info
(defmacro foo2 [x] (keep-callsite `(truss/have ~x))) ; With line info"
{:added "v1.8.0 (2022-12-13)"}
[& body] `(with-meta (do ~@body) (meta ~'&form))))
(comment
(defmacro foo1 [x] `(have ~x))
(defmacro foo2 [x] (keep-callsite `(have ~x)))
(foo1 nil)
(foo2 nil))
(defn get-data
"Returns current value of dynamic assertion data."
[] impl/*data*)
#?(:clj
(defmacro with-data
"Executes body with dynamic assertion data bound to given value.
This data will be included in any violation errors thrown by body."
[data & body] `(binding [impl/*data* ~data] ~@body)))
(comment (with-data "foo" (have string? 5 :data "bar")))
(defn- -error-fn [f] (if (= f :default) impl/default-error-fn f))
(defn set-error-fn!
"Sets the root (fn [data-map-delay]) called on invariant violations."
[f]
#?(:cljs (set! impl/*error-fn* (-error-fn f))
:clj (alter-var-root #'impl/*error-fn* (fn [_] (-error-fn f)))))
#?(:clj
(defmacro with-error-fn [f & body]
`(binding [impl/*error-fn* ~(-error-fn f)] ~@body)))
;;;; Deprecated
(defn get-dynamic-assertion-data
{:deprecated "v1.7.0 (2022-11-16)"
:doc "Prefer `get-data`"}
[] impl/*data*)
#?(:clj
(defmacro with-dynamic-assertion-data
{:deprecated "v1.7.0 (2022-11-16)"
:doc "Prefer `with-data`"}
[data & body] `(binding [impl/*data* ~data] ~@body)))