├── .gitignore
├── Makefile
├── README.md
├── UNLICENSE
├── project.clj
├── src
└── pact
│ ├── comp_future.clj
│ ├── core.cljc
│ ├── core_async.cljc
│ └── manifold.clj
├── test
└── pact
│ └── core_test.cljc
└── trash.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | profiles.clj
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 | /.prepl-port
12 | .hgignore
13 | .hg/
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | .PHONY: test-all
3 | test-all: test test-js
4 |
5 | .PHONY: test
6 | test:
7 | lein test
8 |
9 |
10 | .PHONY: release
11 | release:
12 | lein release
13 |
14 |
15 | test-js:
16 | lein with-profile +cljs cljsbuild once
17 | node target/tests.js
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pact
2 |
3 | A small library for chaining values through forms. It's like a promise but much
4 | simpler.
5 |
6 | Since 0.1.1, supports ClojureScript and its specific types (e.g. Promise).
7 |
8 | ## Installation
9 |
10 | Lein:
11 |
12 | ```clojure
13 | [com.github.igrishaev/pact "0.1.1"]
14 | ```
15 |
16 | Deps.edn
17 |
18 | ```clojure
19 | {com.github.igrishaev/pact {:mvn/version "0.1.1"}}
20 | ```
21 |
22 | ## How It Works
23 |
24 | The library declares two universe handlers: `then` and `error`. When you apply
25 | them to the "good" values, you propagate further. Applying `error` to "good" things
26 | does nothing. And vice versa: `then` for the "bad" values does nothing, but calling
27 | `error` on "bad" values gives you a chance to recover the pipeline.
28 |
29 | By default, there is only one "bad" value which is an instance of `Throwable`
30 | (`js/Error` in ClojureScript). Other types are considered positive ones. The
31 | library carries extensions for such async data types as `CompletableFuture`,
32 | `Manifold` and `core.async`. You only need to require their modules so they
33 | extend the `IPact` protocol.
34 |
35 | ## Examples
36 |
37 | Import `then` and `error` macros, then chain a value with the standard `->`
38 | threading macro. Both `then` and `error` accept a binding vector and an
39 | arbitrary body.
40 |
41 | ```clojure
42 | (ns foobar
43 | (:require
44 | [pact.core :refer [then error]]))
45 |
46 |
47 | (-> 42
48 | (then [x]
49 | (-> x int str))
50 | (then [x]
51 | (str x "/hello")))
52 |
53 | "42/hello"
54 | ```
55 |
56 | If any exception pops up, the sequence of `then` handlers gets interrupted, and
57 | the `error` handler gets into play:
58 |
59 | ```clojure
60 | (-> 1
61 | (then [x]
62 | (/ x 0))
63 | (then [x]
64 | (str x "/hello")) ;; won't be executed
65 | (error [e]
66 | (ex-message e)))
67 |
68 | "Divide by zero"
69 | ```
70 |
71 | The `error` handler gives you a chance to recover from the exception. If you
72 | return a non-exceptional data in `error`, the execution will proceed from the
73 | next `then` handler:
74 |
75 | ```clojure
76 | (-> 1
77 | (then [x]
78 | (/ x 0))
79 | (error [e]
80 | (ex-message e))
81 | (then [message]
82 | (log/info message)))
83 |
84 | ;; nil
85 | ```
86 |
87 | The `->` macro can be nested. This is useful to capture the context for a
88 | possible exception:
89 |
90 | ```clojure
91 | (-> 1
92 | (then [x]
93 | (+ x 1))
94 | (then [x]
95 | (-> x
96 | (then [x]
97 | (/ x 0))
98 | (error [e]
99 | (println "The x was" x)
100 | nil))))
101 |
102 | ;; The x was 2
103 | ;; nil
104 | ```
105 |
106 | Besides `then` and `error` macros, the library provides the `then-fn` and
107 | `error-fn` functions. They are useful when you have a ready function that
108 | processes the value:
109 |
110 | ```clojure
111 | (ns foobar
112 | (:require
113 | [pact.core :refer [then-fn error-fn]]))
114 |
115 | (-> 1
116 | (then-fn inc)
117 | (then-fn str))
118 |
119 | ;; "2"
120 |
121 | (-> 1
122 | (then [x]
123 | (/ x 0))
124 | (error-fn ex-message))
125 |
126 | ;; "Divide by zero"
127 | ```
128 |
129 | Chaining with `then` and `error` is especially good for maps as allowing
130 | destructuring:
131 |
132 | ```clojure
133 | (-> {:db {...} :cassandra {...}}
134 |
135 | ;; Get a user from the database and attach it to the scope.
136 | (then [{:as scope :keys [db]}]
137 | (let [user (jdbc/get-by-id db :users 42)]
138 | (assoc scope :user user)))
139 |
140 | ;; Having a user, get their last items from Cassandra cluster
141 | ;; and attach them to the scope.
142 | (then [{:as scope :keys [cassandra user]}]
143 | (let [items (get-user-items cassandra user)]
144 | (assoc scope :items items)))
145 |
146 | ;; Do something more...
147 | (then [...]
148 | ...))
149 | ```
150 |
151 | ## Fast fail
152 |
153 | To interrupt the chain of `then` handlers, either throw an exception or use the
154 | `failure` function which is just a shortcut for raising a exception. The
155 | function takes a map or a message with a map:
156 |
157 | ```clojure
158 | (ns foobar
159 | (:require
160 | [pact.core :refer [then error failure]]))
161 |
162 | (-> 1
163 | (then [x]
164 | (if (not= x 42)
165 | (failure "It was not 42!" {:x x})
166 | (+ 1 x)))
167 | (error-fn ex-data))
168 |
169 | ;; {:x 1 :ex/type :pact.core/failure}
170 | ```
171 |
172 | ## Supported types
173 |
174 | The `core` namespace declares the `then` and `error` handlers for the `Object`,
175 | `Throwable`, and `java.util.concurrent.Future` types. The `Future` values get
176 | dereferenced when passing to `then`.
177 |
178 | The following modules extend the `IPact` protocol for asynchronous types.
179 |
180 | ### Completable Future (Clojure)
181 |
182 | The module `pact.comp-future` handles the `CompletableFuture` class available
183 | since Java 11. The module also provides its own `future` macro to build an
184 | instance of `CompletableFuture`:
185 |
186 | ```clojure
187 | (-> (future/future 1)
188 | (then [x]
189 | (inc x))
190 | (then [x]
191 | (/ 0 0))
192 | (error [e]
193 | (ex-message e))
194 | (deref))
195 |
196 | "Divide by zero"
197 | ```
198 |
199 | Pay attention: if you fed an instance of `CompletableFuture` to the threading
200 | macro, the result will always be of this type. Thus, there is a `deref` call at
201 | the end.
202 |
203 | Internally, the `then` handler calls for the `.thenApply` method if a future and
204 | the `error` handler boils down to `.exceptionally`.
205 |
206 | ### Manifold (Clojure)
207 |
208 | The `pact.manifold` module makes the handlers work with the amazing Manifold
209 | library and its types. The Pact library doesn't have Manifold dependency: you've
210 | got to add it on your own.
211 |
212 | ```clojure
213 | [manifold "0.1.9-alpha3"]
214 | ```
215 |
216 | ```clojure
217 | (-> (d/future 1)
218 | (then [x]
219 | (/ x 0))
220 | (error [e]
221 | (ex-message e))
222 | (deref))
223 |
224 | "Divide by zero"
225 | ```
226 |
227 | Under the hood, `then` and `error` handlers call the `d/chain` and `d/catch`
228 | macros respectively.
229 |
230 | Once you've put an instance of Manifold deferred, the result will always be a
231 | `Deferred`.
232 |
233 | ### Core.async (Clojure + ClojureScript)
234 |
235 | To make the library work with `core.async` channels, import the
236 | `pact.core-async` module:
237 |
238 | ```clojure
239 | (ns foobar
240 | (:require
241 | [pact.core :refer [then error]]
242 | [pact.core-async]
243 | [clojure.core.async :as a]))
244 | ```
245 |
246 | Like Manifold, the `core.async` dependency should be added by you as well:
247 |
248 | ```clojure
249 | [org.clojure/core.async "1.5.648"]
250 | ```
251 |
252 | Now you can chain channels through the `then` and `error` actions. Internally,
253 | each handler takes exactly one value from a source channel and returns a new
254 | channel with the result. For `then`, exceptions traverse the channels being
255 | untouched. And instead, the `error` handler ignores ordinary values and affects
256 | only exceptions. Quick demo:
257 |
258 | ```clojure
259 | (let [in (a/chan)
260 | out (-> in
261 | (then [x]
262 | (/ x 0))
263 | (error [e]
264 | (ex-message e))
265 | (then [message]
266 | (str "<<< " message " >>>")))]
267 |
268 | (a/put! in 1)
269 |
270 | (a/ (js/Promise.resolve 1)
282 | (then-fn inc)
283 | (then [x]
284 | (js/console.log x)))
285 | ```
286 |
287 | A better example with fetching an HTTP resource:
288 |
289 | ```clojure
290 | (-> (js/fetch "https://some.api.com/data.json")
291 | (then [response]
292 | (.json response))
293 | (then [data]
294 | ...)
295 | (error [e]
296 | (js/console.log ...)))
297 | ```
298 |
299 |
300 | ## Testing
301 |
302 | To run both Clojure and ClojureScript tests, execute `make test-all`. For the
303 | ClojureScript tests, you need Node.js installed.
304 |
305 | © 2022 Ivan Grishaev
306 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
in 49 | (then [x] 50 | (+ 1 x)) 51 | (then [x] 52 | (+ 1 x)) 53 | (then [x] 54 | (str "+" x "+")))] 55 | 56 | (a/go 57 | (a/>! in 1) 58 | (is (= "+3+" (a/ in 68 | (then [x] 69 | (throw (ex-info "Divide by zero" {}))) 70 | (then [x] 71 | 42) 72 | (error [e] 73 | (ex-message e)) 74 | (then [message] 75 | (str "<<< " message " >>>")))] 76 | 77 | (a/go 78 | (a/>! in 1) 79 | (is (= "<<< Divide by zero >>>" (a/ in 89 | (then [x] 90 | (throw (new js/Error "err 1"))) 91 | (then [x] 92 | 42) 93 | (error [e] 94 | (throw (new js/Error "err 2"))) 95 | (error [e] 96 | (ex-message e)) 97 | (then [message] 98 | (str "<<< " message " >>>")))] 99 | 100 | (a/go 101 | (a/>! in 1) 102 | (is (= "<<< err 2 >>>" (a/ in 115 | (then [x] 116 | (+ 1 x)) 117 | (then [x] 118 | (+ 1 x)) 119 | (then [x] 120 | (str "+" x "+")))] 121 | 122 | (a/>!! in 1) 123 | (is (= "+3+" (a/ in 130 | (then [x] 131 | (/ x 0)) 132 | (then [x] 133 | 42) 134 | (error [e] 135 | (ex-message e)) 136 | (then [message] 137 | (str "<<< " message " >>>")))] 138 | 139 | (a/put! in 1) 140 | (is (= "<<< Divide by zero >>>" (a/ in 147 | (then [x] 148 | (/ x 0)) 149 | (then [x] 150 | 42) 151 | (error [e] 152 | (+ 1 (ex-message e))) 153 | (error [e] 154 | (ex-message e)) 155 | (then [message] 156 | (str "<<< " message " >>>")))] 157 | 158 | (a/put! in 1) 159 | (is (str/starts-with? (a/ (new js/Date 1000000000000) 172 | (then [date] 173 | (.toGMTString date))))))) 174 | 175 | (testing "test nil" 176 | 177 | (is (= "" 178 | (-> nil 179 | (error-fn str) 180 | (then-fn str))))) 181 | 182 | (testing "boolean" 183 | 184 | (is (= "false" 185 | (-> true 186 | (then-fn not) 187 | (then-fn str))))) 188 | 189 | (testing "string" 190 | 191 | (is (= "42/hello" 192 | (-> 42 193 | (then [x] 194 | (-> x int str)) 195 | (then [x] 196 | (str x "/hello")))))) 197 | 198 | (is (= "Divide by zero" 199 | (-> 42 200 | (then [x] 201 | (throw (ex-info "Divide by zero" {}))) 202 | (then [x] 203 | (+ x 1000)) 204 | (error [e] 205 | (ex-message e))))) 206 | 207 | #?(:clj 208 | (testing "future" 209 | 210 | (is (= 103 211 | (-> (future (+ 1 2)) 212 | (then [x] 213 | (+ x 100))))))) 214 | 215 | (testing "error in error handler" 216 | 217 | (is (= "class java.lang.String cannot be cast to class java.lang.Number" 218 | (-> 42 219 | (then [x] 220 | (throw (ex-info "Divide by zero" {}))) 221 | (error [e] 222 | (throw (ex-info "class java.lang.String cannot be cast to class java.lang.Number" {}))) 223 | (error [e] 224 | (ex-message e)) 225 | (then [message] 226 | (subs message 0 63)))))) 227 | 228 | (testing "stop on error" 229 | 230 | (let [e 231 | (-> 0 232 | inc 233 | inc 234 | (then [x] 235 | (throw (ex-info "Divide by zero" {}))) 236 | (then [x] 237 | (inc x)) 238 | (then [x] 239 | (throw (ex-info "foobar" {}))) 240 | (then [x] 241 | (inc x)) 242 | (then [x] 243 | (inc x)) 244 | (then [x] 245 | (inc x)))] 246 | 247 | (is (= "Divide by zero" (ex-message e)))))) 248 | 249 | 250 | (deftest test-failure-ok 251 | 252 | (let [res 253 | (-> 1 254 | (then [x] 255 | (inc x)) 256 | (then [x] 257 | (failure {:foo 42})) 258 | (error [e] 259 | (ex-data e)))] 260 | 261 | (is (= {:foo 42 :ex/type :pact.core/failure} 262 | res)))) 263 | 264 | 265 | (deftest test-mapping-ok 266 | 267 | (is (= {:a 1 :b 2 :c 3 :d 6} 268 | 269 | (-> {:a 1 :b 2} 270 | 271 | (then [{:as scope :keys [a b]}] 272 | (assoc scope :c (+ a b))) 273 | 274 | (then [{:as scope :keys [a b c]}] 275 | (assoc scope :d (+ a b c))))))) 276 | 277 | 278 | #?(:clj 279 | 280 | (deftest test-comp-future-ok 281 | 282 | (testing "simple" 283 | 284 | (let [fut 285 | (future/future 1) 286 | 287 | res 288 | (-> fut 289 | (then [x] 290 | (inc x)))] 291 | 292 | (is (= 2 @res)))) 293 | 294 | (testing "ex type for comp future" 295 | 296 | (let [fut 297 | (future/future 1) 298 | 299 | res 300 | (-> fut 301 | (then [x] 302 | (inc x)) 303 | (then [x] 304 | (/ 0 0)) 305 | (error [e] 306 | e))] 307 | 308 | (is (= java.lang.ArithmeticException 309 | (-> res deref class))) 310 | 311 | (is (= "Divide by zero" 312 | (-> res deref ex-message)))) 313 | 314 | (testing "recover" 315 | 316 | (let [fut 317 | (future/future 1) 318 | 319 | res 320 | (-> fut 321 | (then [x] 322 | (inc x)) 323 | (then [x] 324 | (/ 0 0)) 325 | (error [e] 326 | (ex-message e)) 327 | (then [message] 328 | (str "<<< " message " >>>")))] 329 | 330 | (is (= "<<< Divide by zero >>>" 331 | @res)))) 332 | 333 | (testing "error in error" 334 | 335 | (let [fut 336 | (future/future 1) 337 | 338 | res 339 | (-> fut 340 | (then [x] 341 | (inc x)) 342 | (then [x] 343 | (/ 0 0)) 344 | (error [e] 345 | (+ 1 (ex-message e))) 346 | (error [e] 347 | (ex-message e)) 348 | (then [message] 349 | (str "<<< " message " >>>")))] 350 | 351 | (is (str/starts-with? 352 | @res "<<< class java.lang.String cannot be cast to class java.lang.Number"))))))) 353 | 354 | 355 | #?(:clj 356 | 357 | (deftest test-manifold-ok 358 | 359 | (testing "simple" 360 | 361 | (let [res 362 | (-> (d/future 1) 363 | (then [x] 364 | (inc x)) 365 | (then-fn inc))] 366 | 367 | (d/deferred? res) 368 | 369 | (is (= 3 @res)))) 370 | 371 | (testing "recovery" 372 | 373 | (let [res 374 | (-> (d/future 1) 375 | (then [x] 376 | (/ x 0)) 377 | (error [e] 378 | (ex-message e)))] 379 | 380 | (d/deferred? res) 381 | 382 | (is (= "Divide by zero" @res)))) 383 | 384 | (testing "error in error" 385 | 386 | (let [res 387 | (-> (d/future 1) 388 | (then [x] 389 | (/ x 0)) 390 | (error [e] 391 | (+ 1 (ex-message e))) 392 | (error [e] 393 | (ex-message e)))] 394 | 395 | (d/deferred? res) 396 | 397 | (is (str/starts-with? 398 | @res "class java.lang.String cannot be cast to class java.lang.Number")))))) 399 | 400 | 401 | 402 | #?(:cljs 403 | 404 | (do 405 | 406 | (deftest test-promise-ok 407 | 408 | (async done 409 | 410 | (let [p 411 | (-> (js/Promise.resolve 1) 412 | (then-fn inc) 413 | (then [x] 414 | (str "<<< " x " >>>")))] 415 | 416 | (a/go 417 | (is (= "<<< 2 >>>" (
(js/Promise.resolve 1) 427 | (then [x] 428 | (throw (ex-info "error" {}))) 429 | (error [e] 430 | (ex-message e)))] 431 | 432 | (a/go 433 | (is (= "error" (
(js/Promise.resolve 1) 443 | (then [x] 444 | (throw (ex-info "error1" {}))) 445 | (error [e] 446 | (throw (ex-info "error2" {}))) 447 | (error [e] 448 | (ex-message e)))] 449 | 450 | (a/go 451 | (is (= "error2" (