Skip to content

igrishaev/pact

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pact

A small library for chaining values through forms. It's like a promise but much simpler.

Since 0.1.1, supports ClojureScript and its specific types (e.g. Promise).

Installation

Lein:

[com.github.igrishaev/pact "0.1.1"]

Deps.edn

{com.github.igrishaev/pact {:mvn/version "0.1.1"}}

How It Works

The library declares two universe handlers: then and error. When you apply them to the "good" values, you propagate further. Applying the error for does nothing. And vice versa: then for the "bad" values does nothing, but calling error on "bad" values gives you a chance to recover the pipeline.

By default, there is only one "bad" value which is an instance of Throwable (js/Error in ClojureScript). Other types are considered positive ones. The library carries extensions for such async data types as CompletableFuture, Manifold and core.async. You only need to require their modules so they extend the IPact protocol.

Examples

Import then and error macros, then chain a value with the standard -> threading macro. Both then and error accept a binding vector and an arbitrary body.

(ns foobar
  (:require
   [pact.core :refer [then error]]))


(-> 42
    (then [x]
      (-> x int str))
    (then [x]
      (str x "/hello")))

"42/hello"

If any exception pops up, the sequence of then handlers gets interrupted, and the error handler gets into play:

(-> 1
    (then [x]
      (/ x 0))
    (then [x]
      (str x "/hello")) ;; won't be executed
    (error [e]
      (ex-message e)))

"Divide by zero"

The error handler gives you a chance to recover from the exception. If you return a non-exceptional data in error, the execution will proceed from the next then handler:

(-> 1
    (then [x]
      (/ x 0))
    (error [e]
      (ex-message e))
    (then [message]
      (log/info message)))

;; nil

The -> macro can be nested. This is useful to capture the context for a possible exception:

(-> 1
    (then [x]
      (+ x 1))
    (then [x]
      (-> x
          (then [x]
            (/ x 0))
          (error [e]
            (println "The x was" x)
            nil))))

;; The x was 2
;; nil

Besides then and error macros, the library provides the then-fn and error-fn functions. They are useful when you have a ready function that processes the value:

(ns foobar
  (:require
   [pact.core :refer [then-fn error-fn]]))

(-> 1
    (then-fn inc)
    (then-fn str))

;; "2"

(-> 1
    (then [x]
      (/ x 0))
    (error-fn ex-message))

;; "Divide by zero"

Chaining with then and error is especially good for maps as allowing destructuring:

(-> {:db {...} :cassandra {...}}

    ;; Get a user from the database and attach it to the scope.
    (then [{:as scope :keys [db]}]
      (let [user (jdbc/get-by-id db :users 42)]
        (assoc scope :user user)))

    ;; Having a user, get their last items from Cassandra cluster
    ;; and attach them to the scope.
    (then [{:as scope :keys [cassandra user]}]
      (let [items (get-user-items cassandra user)]
        (assoc scope :items items)))

    ;; Do something more...
    (then [...]
      ...))

Fast fail

To interrupt the chain of then handlers, either throw an exception or use the failure function which is just a shortcut for raising a exception. The function takes a map or a message with a map:

(ns foobar
  (:require
   [pact.core :refer [then error failure]]))

(-> 1
    (then [x]
      (if (not= x 42)
        (failure "It was not 42!" {:x x})
        (+ 1 x)))
    (error-fn ex-data))

;; {:x 1 :ex/type :pact.core/failure}

Supported types

The core namespace declares the then and error handlers for the Object, Throwable, and java.util.concurrent.Future types. The Future values get dereferenced when passing to then.

The following modules extend the IPact protocol for asynchronous types.

Completable Future (Clojure)

The module pact.comp-future handles the CompletableFuture class available since Java 11. The module also provides its own future macro to build an instance of CompletableFuture:

(-> (future/future 1)
    (then [x]
      (inc x))
    (then [x]
      (/ 0 0))
    (error [e]
      (ex-message e))
    (deref))

"Divide by zero"

Pay attention: if you fed an instance of CompletableFuture to the threading macro, the result will always be of this type. Thus, there is a deref call at the end.

Internally, the then handler calls for the .thenApply method if a future and the error handler boils down to .exceptionally.

Manifold (Clojure)

The pact.manifold module makes the handlers work with the amazing Manifold library and its types. The Pact library doesn't have Manifold dependency: you've got to add it on your own.

[manifold "0.1.9-alpha3"]
(-> (d/future 1)
    (then [x]
      (/ x 0))
    (error [e]
      (ex-message e))
    (deref))

"Divide by zero"

Under the hood, then and error handlers call the d/chain and d/catch macros respectively.

Once you've put an instance of Manifold deferred, the result will always be a Deferred.

Core.async (Clojure + ClojureScript)

To make the library work with core.async channels, import the pact.core-async module:

(ns foobar
  (:require
   [pact.core :refer [then error]]
   [pact.core-async]
   [clojure.core.async :as a]))

Like Manifold, the core.async dependency should be added by you as well:

[org.clojure/core.async "1.5.648"]

Now you can chain channels through the then and error actions. Internally, each handler takes exactly one value from a source channel and returns a new channel with the result. For then, exceptions traverse the channels being untouched. And instead, the error handler ignores ordinary values and affects only exceptions. Quick demo:

(let [in (a/chan)
      out (-> in
              (then [x]
                (/ x 0))
              (error [e]
                (ex-message e))
              (then [message]
                (str "<<< " message " >>>")))]

  (a/put! in 1)

  (a/<!! out) )

;; "<<< class java.lang.String cannot be cast ..."

JS Promise (ClojureScript)

For a JS promise, then and error handlers resolve to its .then and .catch methods:

(-> (js/Promise.resolve 1)
    (then-fn inc)
    (then [x]
      (js/console.log x)))

A better example with fetching an HTTP resource:

(-> (js/fetch "https://some.api.com/data.json")
    (then [response]
      (.json response))
    (then [data]
      ...)
    (error [e]
      (js/console.log ...)))

Testing

To run both Clojure and ClojureScript tests, execute make test-all. For the ClojureScript tests, you need Node.js installed.

© 2022 Ivan Grishaev