danielsz.benjamin

https://github.com/danielsz/benjamin.git

git clone 'https://github.com/danielsz/benjamin.git'

(ql:quickload :danielsz.benjamin)
27

** Timed idempotency with side-effects

+HTML:

Benjamin gives you a macro that transforms code like this:

+BEGIN_SRC clojure

(let [logbook (get-logbook entity)] (when (some pred logbook) (let [response (body)] (when (success? response) (write logbook)))))

+END_SRC

Into this:

+BEGIN_SRC clojure

(with-logbook entity :event body)

+END_SRC

There is a [[http://danielsz.github.io/2017/08/07/Timed-idempotency][blog]] post that delves in the motivation and backstory.

* Installation [[https://clojars.org/org.danielsz/benjamin/latest-version.svg]] * Usage

In your namespace, require:

+BEGIN_SRC clojure

[benjamin.core :refer [with-logbook]]

+END_SRC

** Configuration

Tip: ~system~ users can configure this library via a [[https://github.com/danielsz/system/blob/f4acb68d1e136720c1f9ab44d65e2eb763b1e6ef/src/system/components/benjamin.clj][component]] that ships with the latest snapshot.

Manual configuration is done by requiring:

+BEGIN_SRC clojure

[benjamin.configuration :refer [set-config!]]

+END_SRC

* Accessing the logbook

+BEGIN_SRC clojure

(set-config! :logbook-fn f)

+END_SRC

~logbook-fn~ is a function that receives the entity as argument and returns a logbook. The default is ~:logbook~ which will work when the entity map embeds the logbook, as in:

+BEGIN_SRC clojure

{:first-name “Benjamin” :last-name “Peirce” :occupation “Mathematician” :email “benjamin.peirce@harvard.edu” :logbook [{:welcome-email timestamp} {:subscription-reminder timestamp} {:subscription-reminder timestamp} {:newsletter timestamp} {:newsletter timestamp} {:newsletter timestamp}]}

+END_SRC

* Deriving predicates from events

+BEGIN_SRC clojure

(set-config! :events {:event predicate :event predicate :event predicate …})

+END_SRC

Predicates are one argument functions that receive a logbook entry. A logbook entry is a map with an event as the key and a timestamp as the value.

The following example checks if the logbook entry was written today.

+BEGIN_SRC clojure

(if-let [date (first (vals %))]

(time/today? date) false)

+END_SRC

Several predicates are offered in the ~benjamin.predicates~ namespace for convenience. That namespace has a dependency that you need to require in your build should you want to use them. This is because ~benjamin~ does not have any dependency of its own (it relies entirely on language features).

[[https://clojars.org/org.danielsz/detijd/latest-version.svg]]

** Writing to the logbook

~:persistence-fn~ is a function of two arguments, ~entity~ and ~event~. Its responsibility is to append to the logbook and persist the entity. You have to provide an implementation or an error will be thrown. For example:

+BEGIN_SRC clojure

(set-config! :persistence-fn (fn [entity event] (let [logbook (conj (:logbook entity) {event (t/now)})] (assoc entity :logbook logbook) (save db entity))))

+END_SRC

Tip: If you have dependencies (as a reference to the db), use a higher–order function that returns ~persistence-fn~.

+BEGIN_SRC clojure

(defn logbook [{{store :store} :db :as dependencies}] (fn [entity event] (let [logbook (conj (:logbook entity) {event (t/now)})] (assoc entity :logbook logbook) (save db entity)))

+END_SRC

Tip: The ~benjamin~ component in the ~system~ library includes an option that will wrap dependencies associated with it in the system map.

** Determining the status of the side-effect

The success function is a function of one argument, ie. the return value of the side-effectful body. It determines if the operation was successful and thus for inclusion in the logbook.

+BEGIN_SRC clojure

(set-config! :success-fn (constantly true))

+END_SRC

The default assumes all your operations will be A-okay. You’ll probably want to pass along something more realistic.

** Strict or lax policy with unknown events

+BEGIN_SRC clojure

(with-logbook entity event body)

+END_SRC

If the event is unkown, that is if it doesn’t show up in the events map, no predicate can be derived and then we rely on a policy you can set yourself. Either we accept unknown events and we proceed with the side-effect, or we reject them and return immediately. The default is strict, but you can change that.

+BEGIN_SRC clojure

(set-config! :allow-undeclared-events? true)

+END_SRC

** Tests

A test suite is provided in ~benjamin.core-test~. Call ~(test-ns ns)~ in the namespace, or run ~boot testing~ for continous testing.

** Limitations

You can work with as many entities you want. You can declare as many events as you want. You can have any side-effectful procedures in the body. Your ~success-fn~ may dispatch on the return value if you run different types of operations in the body.

The configuration is a singleton with dynamic scope, so deal with it to the best of your understanding. Personally, I set it once and treat it as a constant for the lifetime of the application.

** License Licensing terms will be revealed shortly. In the meantime, do what you want with it.