Liking cljdoc? Tell your friends :D

slip

Build Status Clojars Project cljdoc badge

A bit like clip's degenerate one-trick cousin.

slip is a Clojure+Script IOC micro-library which builds a system of objects. It transforms a pure-data system specification into a pure-data interceptor-chain description and then runs an asynchronous interceptor-chain to create a system map. Errors during construction of the system map cause the operation to be unwound gracefully, avoiding leaving any objects in unknown states.

why?

There are a few IOC libs around for Clojure and ClojureScript - Component, Mount, Integrant and Clip. See Clip's README for a detailed comparison of the features of these libs.

Only Clip attempts to deal with asynchronous (i.e. promise returning) factory functions. Slip takes a similar approach to Clip and deals well with asynchronous factories, but it uses a different extension mechanism - avoiding code-as-data difficulties on ClojureScript, but making other trade-offs in the process.

The system specification

Slip builds system maps. A system map will have keyword keys and is built according to a system specification - a SystemSpec, which is governed by a Malli schema.

A SystemSpec is a collection of ObjectSpecs. An ObjectSpec describes how to create and destroy an individual object in a system map. Each ObjectSpec provides:

  • :slip/key - the <object-key> - a keyword key for the object in the system map
  • :slip/factory - optional <factory-key> keyword - identifies a lifecycle method for creating and destroying the object. Defaults to <object-key>
  • :slip/data - a DataSpec template for data provided to lifecycle methods
    • the templates supports keyword-maps, vectors, references to other system objects and literal values.

A SystemSpec can be given in map form, with implicit :slip/keys:

{:foo {:slip/data #slip/ref [:config :foo]}
 :bar {:slip/factory :barfac
       :slip/data {:f #slip/ref :foo
                   :cfg #slip/ref [:config :bar]}}}

or, equivalently, a system can be specified in a vector form with explicit :slip/keys:

[{:slip/key :foo :slip/data #slip/ref [:config :foo]}
 {:slip/key :bar
  :slip/factory :barfac
  :slip/data {:f #slip/ref :foo
              :cfg #slip/ref [:config :bar]}}]

lifecycle methods

Objects in a system map are created and destroyed by the factory lifecycle methods. There are two lifecycle methods, start and stop, and method dispatch is on an ObjectSpecs <factory-key> (which defaults to the <object-key>). The lifecycle method implementations should either return constructed objects directly, or return a promise of the constructed object.

For a given <factory-key>, a start method is required, but a stopmethod is optional (so need not be provided when no resource cleanup is necessary).

The lifecycle method signatures are:

(defmulti start (fn [<factory-key> <data>]))
(defmulti stop (fn [<factory-key> <data> <object>]))

and an application should provide implementations of these methods for each type of object to be managed.

lifecycle method data and system refs

The <data> parameter for lifecycle methods is built according to the :slip/data DataSpec template from an ObjectSpec.

DataSpec templates are expanded with any #slip/ref references replaced with the value referred to from the (under-construction) system map - thus a DAG of objects can be created.

Slip identifies object dependencies and will start/stop objects in such an order that all dependencies can be met.

If a reference points to a nil location in the system map then an error will be thrown. If nil is a valid value for the reference then using a #slip/ref? will not cause an error.

Example

(require '[promesa.core :as p])
(require '[slip.multimethods :as mm])
(require '[slip.core :as slip])

(def sys
 {:foo {:slip/data #slip/ref [:config :foo]}
  :bar {:slip/factory :barfac
        :slip/data {:f #slip/ref :foo
                    :cfg #slip/ref [:config :bar]}}})

(defmethod mm/start :foo
  [k d]
  (p/delay 100 d))

(defmethod mm/start :barfac
  [k {f :f
      cfg :cfg
      :as d}]
  (p/delay
    100
    {:foo f
     :bar-cfg cfg}))

(def app @(slip/start sys {:config {:foo 100 :bar 200}}))

app ;; => {:config {:foo 100, :bar 200},
    ;;     :foo 100,
    ;;     :bar {:foo 100, :bar-cfg 200}}

ClojureScript

It's common for JavaScript object factories to return a Promise of their result. Slip is fully Promise compatible - the interceptor chain is promise-based and any of the lifecycle fns can return a promise of their result, as the above example demonstrates.

debugging

You can see exactly what happened during construction of your system by providing a:slip/debug? option to start

(def app @(slip/start
           sys
           {:config {:foo 100 :bar 200}}
           {:slip/debug? true}))

your system will get a :slip/log key with a detailed breakdown of the interceptor fns called, with what data and what outcome. Here's the log for the example above - each log entry has: [ObjectSpec <interceptor-fn> <interceptor-action> <data> <outcome>]

[[#:a-frame.interceptor-chain{:key :slip.system/start,
                              :enter-data
                              #:slip{:data #slip/ref [:config :foo],
                                     :key :foo}}
  :a-frame.interceptor-chain/enter
  :a-frame.interceptor-chain/execute
  #:slip{:data 100, :key :foo}
  :a-frame.interceptor-chain/success]

 [#:a-frame.interceptor-chain{:key :slip.system/start,
                              :enter-data
                              #:slip{:factory :barfac,
                                     :data
                                     {:f #slip/ref [:foo],
                                      :cfg #slip/ref [:config :bar]},
                                     :key :bar}}
  :a-frame.interceptor-chain/enter
  :a-frame.interceptor-chain/execute
  #:slip{:factory :barfac, :data {:f 100, :cfg 200}, :key :bar}
  :a-frame.interceptor-chain/success]

 [#:a-frame.interceptor-chain{:key :slip.system/start,
                              :enter-data
                              #:slip{:factory :barfac,
                                     :data
                                     {:f #slip/ref [:foo],
                                      :cfg #slip/ref [:config :bar]},
                                     :key :bar}}
  :a-frame.interceptor-chain/leave
  :a-frame.interceptor-chain/noop
  :_
  :a-frame.interceptor-chain/success]

 [#:a-frame.interceptor-chain{:key :slip.system/start,
                              :enter-data
                              #:slip{:data #slip/ref [:config :foo],
                                     :key :foo}}
  :a-frame.interceptor-chain/leave
  :a-frame.interceptor-chain/noop
  :_
  :a-frame.interceptor-chain/success]]

Should there be an error during system construction you won't get the system map back directly - instead you will get an errored promise, with ex-data with a ::context key - which will contain the interceptor chain context, which has the log.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close