https://github.com/danielsz/benjamin.git
git clone 'https://github.com/danielsz/benjamin.git'
(ql:quickload :danielsz.benjamin)
** Timed idempotency with side-effects
Benjamin gives you a macro that transforms code like this:
(let [logbook (get-logbook entity)] (when (some pred logbook) (let [response (body)] (when (success? response) (write logbook)))))
Into this:
(with-logbook entity :event body)
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:
[benjamin.core :refer [with-logbook]]
** 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:
[benjamin.configuration :refer [set-config!]]
* Accessing the logbook
(set-config! :logbook-fn f)
~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:
{: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}]}
* Deriving predicates from events
(set-config! :events {:event predicate :event predicate :event predicate …})
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.
(time/today? date) false)
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:
(set-config! :persistence-fn (fn [entity event] (let [logbook (conj (:logbook entity) {event (t/now)})] (assoc entity :logbook logbook) (save db entity))))
Tip: If you have dependencies (as a reference to the db), use a higher–order function that returns ~persistence-fn~.
(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)))
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.
(set-config! :success-fn (constantly true))
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
(with-logbook entity event body)
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.
(set-config! :allow-undeclared-events? true)
** 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.