• Stars
    star
    228
  • Rank 175,267 (Top 4 %)
  • Language
    Clojure
  • License
    MIT License
  • Created about 5 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Light structure and support for dependency injection

Clip

Clojars Project cljdoc badge

Clip is an inversion of control API which minimizes impact on your code. It is an alternative to integrant, mount or component.

Background

Project Status

Alpha. The core API is unlikely to change significantly, but there may be some small changes to the system-config format. Design feedback is still welcome, there is still space for the rationale to change/expand. While bugs are avoided, the immaturity of the project makes them more likely. Please provide design feedback and report bugs on the issue tracker.

Rationale

System as data

Systems may fail during startup, which can leave you in a state where a port is in use, but you have no handle to that partially-started system in order to close it.

(let [db (get-db)]
      http (start-http-server db)
      ;; What happens if make-queue-listener throws an exception?
      queue-listener (make-queue-listener)]
  โ€ฆ)

This can lead to re-starting the REPL, which is a major interruption to flow. Clip provides a data model for your system, and will rethrow exceptions with the partially-started components attached in order to allow automatic recovery in the REPL or during tests.

You may choose to make your system very granular. For example, you may choose to provide each web request handler with itโ€™s dependencies directly, rather than passing them via the router. In these cases, you will find your system quickly grows large, having your system as data reduces the effort to maintain & understand the relationships between components.

EDN is a natural way to express data in Clojure. A data-driven system should naturally work with EDN without any magic.

Boilerplate

Existing dependency injection libraries require boilerplate for defining your components. This either comes in the form of multi-methods which must have their namespaces required for side-effects, or the creation of records in order to implement a protocol. These extension mechanisms are used to extend existing var calls in libraries. Instead of doing that, we can take a reference to a function (e.g. ring.adapter.jetty/run-jetty) and call it directly. This means that effort to wrap an existing library is minimal, you simply have to specify what to call. This also removes the problem of inventing new strategies for tying a keyword back to a namespace as Integrant has to, by directly using the fully qualified symbol which already includes the namespace as required. In addition, normal functions support doc-strings, making for easy documentation. Finally, this use of vars means that your library is not coupled to Clip in any way, yet itโ€™s easy to use directly from Clip.

Transitions

Side-effects are part of a startup process. The most common example is seeding a database. Before other components can function (such as a health check) the migration must have run. Existing approaches require us to either taint our componentโ€™s start with additional side-effect code (complecting connection with migration) or to create side-effecting components which others must then depend upon.

Clip by default, provides a :pre and :post phase for start-up, enabling you to run setup before/after starting the component. For example, you may need to call datomic.api/create-database before connecting to it, and you may want to call my.app/seed-db after starting it, but before passing it around.

Clip is also simple, it separates out running many actions on your system. This allows you to define custom phases for components, if you need them.

Async

Async is a significant part of systems when doing ClojureScript applications. Something as simple as reading data from disk or fetching the user from a remote endpoint will cause your entire system to be aware of the callback.

Interceptors have shown that async can be a layered-on concern, rather than being intrinsically present in all consumption of the result. Clip provides multiple "executors" for running actions against your system, providing out of the box support for sync (no async), promesa, and manifold. If you need an additional executor, they are fairly simple to write.

Obvious

Obvious connections between actions are easier to understand than obscure ones. Use of inheritance for references or relying on implicit dependencies increases the obscurity of your API. Component and Integrant allow you to spread out your references through the use of using and prep-key. Instead Clip encourages you to make references live in the system, making it always obvious how components connect together.

Comparison with other libraries

Name Need/has wrappers for libs Extension mechanism Suspension System location Multiple parallel systems? Transparent async between components?

Component

Yes

Protocol

Via library

Map

Yes

No

Integrant

Yes

Multi-method

Yes

Map

Yes

No

Clip

No

Code/code as data

Coming

Map

Yes

Yes

Mount

No

Code

No

Namespace

No

No

Usage

Defining a system configuration

You define a system configuration with data. A system configuration contains a :components key and an optional :executor key. :components is a map from anything to a map of properties about that component. Note the use of ` in the example below, this is to prevent execution, you might find it easier to use Use with EDN to define your system configuration.

(def system-config
  {:components
   {:db {:pre-start `(d/create-database "datomic:mem://newdb") ;; (1)
         :start `(d/connect "datomic:mem://newdb") ;; (2)
         :post-start `seed-conn} ;; (3)
    :handler {:start `(find-seed-resource (clip/ref :db))} ;; (4)
    :http {:start `(yada/listener (clip/ref :handler))
           :stop '((:close this)) ;; (5)
           :resolve :server} ;; (6)
    :foo {:start '(clip/ref :http)}}})
  1. :pre-start will be run before :start for your component. Here we use it to run the required create-database in datomic.

  2. :start is run and returns the value that other components will refer to.

  3. :post-start is run before passing the component to any other components. Here, we use it to seed the connection. Because we provided a symbol, it will be resolved and called like so (seed-conn conn) where conn is the started component.

  4. Here we use (clip/ref) to refer to the :db component. This will be provided positionally to the function.

  5. :stop has access to the variable this to refer to the started component.

  6. You can control how a component is referenced by other components. Here the :server key will be passed to other components referencing it (e.g. :foo).

:components reference

Out of the box, there are a handful of keys supported for a component. In the future this may be more extensible (please open an issue if you have a use-case!).

Many of the values of these keys take code as data. This means that if you were to create them programmatically you have to create them using either list or quotes. Supporting code as data means that EDN-based systems can be defined, but also that thereโ€™s special execution rules.

Code as data means that you donโ€™t need to use actual function references, you can use symbols and these will be required & resolved by Clip. Requiring and resolving isnโ€™t supported in ClojureScript, see workaround in ClojureScript.

Code is either executed with an "implicit target" or not. An example of an implicit target is the started instance. If an implicit target is available, you can provide a symbol or function without a list and it will be called with an argument which is the implicit target. If the function to call has multiple arguments, then you can use this to change where the implicit target will be placed.

Key Implicit Target Description

:pre-start

No

Run before starting the component

:start

No

Run to start the component, this will be what ends up in the system

:post-start

Started instance

Run with the started component, a useful place to perform migrations or seeding

:stop

Started instance (to stop)

Run with the started component, should be used to shut down the component. Optional to add. If not specified and value is AutoCloseable, then .close will be run on it

:resolve

Started instance

Run with the started component used by other components to get the value for this component when using (clip/ref)

Supported values for code as data with implicit target:

Type Description

Symbol

Resolved to function then called with target

Function

Resolved to function then called with target

Keyword

Used to get the key out of the target

Supported values for code as data without implicit target:

Type Description Example(s)

Symbol

Resolved to function and called with no arguments

'myapp.core/start-web-server

Function

Called with no arguments

myapp.core/start-web-server

Vector

Recursed into, with arguments resolved

[(clip/ref :foo)]

List

Called as if code

(list 'myapp.core/start-web-server {:port 8000}) '(myapp.core/start-web-server {:port 8000})

:else

Returned unchanged

Async Components

In Clip, async is achieved by using alternative executors. Out of the box support is provided for promesa and manifold. Open an issue if youโ€™d like to see support for another popular library.

Executors are specified on your system and must be a function. juxt.clip.edn/load will convert your :executor from a symbol to a function.

Example 1. Promesa Async Example
{:executor juxt.clip.promesa/exec
 :components
 {:a {:start `(promesa.core/resolved 10)}
  :b {:start `(inc (clip/ref :a))}}}

Note that :b does not need to be aware that :a returns an async value. It will be called at the appropriate time with the resolved value.

Example 2. Manifold Example
(require '[manifold.deferred :as d])

{:executor juxt.clip.manifold/exec
 :components
 {:a {:start `(d/chain 10)}
  :b {:start `(inc (clip/ref :a))}}}

Reloaded Workflow

Clip provides a namespace for easily setting up a reloaded workflow. You will need to add a dependency on tools.namespace to your project.

You should call set-init! with a function which will return your system-config. Usually you will have such a function defined in another namespace that takes a "profile" or "config" in order to be parameterized to development or production. If you are using Use with EDN to load your system, ensure your function calls clip.edn/load.

(ns dev
  (:require
    [app.system]
    [juxt.clip.repl :refer [start stop reset set-init! system]]))

(set-init! #(app.system/system-config :dev))

Async executors

If youโ€™re using an async executor with the repl namespace, you may need to make it sync. Out of the box, the repl namespace will do itโ€™s best to work with anything supported by deref. If you need to override the deref that the repl namespace uses, you can supply a symbol or function in the key :juxt.clip.repl/deref. It should take one argument: the system to deref.

You wonโ€™t need to tweak this for Promesa or Manifold.

(set-init!
  (constantly {:executor juxt.clip.awkward-async/exec
               :juxt.clip.repl/deref some.ns.awkard-async/deref
               โ€ฆ}))

ClojureScript

ClojureScript has limitations with taking code-forms as data. This will continue to be an active research topic, but until resolved the usage is still reasonably concise. You must use list to create a list-form.

Example 3. ClojureScript Usage
(ns frontend.core
  (:require [juxt.clip.core :as clip]))

(def system-config
  {:components
    {:foo {:start 200}
     :bar {:start (list inc (clip/ref :foo))}}})
Caution
The following macro is experimental, feedback on use is welcome. However, of the following experimental options it is currently the forerunner.

There is a macro called with-deps that allows you to write a code-form and bind the dependencies required. This is useful when using Clip from a code (rather than data) context. Itโ€™s also particularly useful in ClojureScript where symbols cannot be resolved back to functions.

with-deps takes bindings and a body, much like fn. The first of the bindings must be to the deps you want. You must use associative destructuring.

Example 4. with-deps Usage
(ns frontend.core
  (:require [juxt.clip.core :as clip :include-macros true]))

(def system-config
  {:components
    {:foo {:start 200}
     :bar {:start 300}
     :baz {:start (clip/with-deps [{:keys [foo bar]}]
                    (+ foo bar))}}})
Caution
The following macro is extremely experimental, feed-back on use is welcome.

You can also bring in the deval macro. This macro will convert lists of code it finds into non-evaluated lists, which can later be interpreted by Clip.

Example 5. Deval Usage
(ns frontend.core
  (require '[juxt.clip.core :as clip :include-macros true]))

(def system-config
  (clip/deval
    {:components
      {:foo {:start 200}
       :bar {:start (inc (clip/ref :foo))}}}))

How to

Use with EDN

Clip works very well with EDN. First, you must call juxt.clip.edn/load on your EDN system to produce a system Clip can understand. This will convert your EDN structure into a system compatible with juxt.clip.core functions by, e.g. converting :executor from a symbol to a function.

It was designed to be used with a library such as aero in order to make dev/prod changes to your system. Hereโ€™s a minimal example system-config configured with aero:

config.edn
{:system-config
 {:components
  {:db {:start (hikari-cp.core/make-datasource
                 #profile
                 {:dev
                  {:adapter "h2"
                   :url "jdbc:h2:~/test"}
                  :prod
                  {:jdbc-url "jdbc:sqlite:db/database.db"}})
        :stop (hikari-cp.core/close-datasource this)}}}}

Use from -main

When starting your application from -main thereโ€™s a few considerations:

  • Blocking forever (Use @(promise) to do this)

  • Storing the system for REPLing in later

  • Whether to shutdown the system or not

Simplest version, blocking forever
(ns myapp.main
  (:require
    [myapp.system]
    [juxt.clip.core :as clip]))

(defn -main
  [& _]
  (clip/start (myapp.system/system-config :prod))
  @(promise))
Storing system for later
(ns myapp.main
  (:require
    [myapp.system]
    [juxt.clip.core :as clip]))

(def system nil)

(defn -main
  [& _]
  (let [system (clip/start (myapp.system/system-config :prod))]
    (alter-var-root #'system (constantly system)))
  @(promise))
Stopping system on shutdown
(ns myapp.main
  (:require
    [myapp.system]
    [juxt.clip.core :as clip]))

(def system nil)

(defn -main
  [& _]
  (let [system-config (myapp.system/system-config :prod)
        system (clip/start system-config)]
    (alter-var-root #'system (constantly system))
    (.addShutdownHook
     (Runtime/getRuntime)
     (Thread. #(clip/stop system-config system))))
  @(promise))

AOT compile namespaces

You need to involve Clip in the AOT process in order to require any namespaces required by your system.

(binding [*compile-files* true] ;; (1)
  (-> ((requiring-resolve 'myapp.system/get-system)) ;; (2)
      clip.edn/load ;; (3)
      clip/require ;; (4)
      ))
  1. Tell Clojure that when we require code, weโ€™d like to AOT it.

  2. We must require myapp.system inside of the binding otherwise it wonโ€™t be AOTโ€™d.

  3. Optional, only do this if your system is in EDN.

  4. This is what requires the namespaces used by your system.

Test systems

Clip provides 2 useful mechanisms for testing:

  1. select to get a subset of a system-config with only the specified components. start also takes a list of components to start as a convenience.

  2. with-system macro which can start a system, and tries very hard to close it.

For example, you may wish to test a single ring handler, and pass it any required arguments to start:

(ns my.app.handler-test
  (:require
    [clojure.test :refer [deftest is]]
    [juxt.clip.core :as clip]
    [my.app.system :refer [get-system]]
    [ring.mock.request :as mock]))

(deftest handler-test
  (clip/with-system [system (clip/select (get-system) [:my-handler])]
    (try
      (is (= {} (:my-handler system)))
      (finally
        (clip/stop system)))))

Custom reloaded workflow

Alternatively you can roll your own Reloaded workflow quite easily, but you will miss out on convenient features in the built-in one like auto-cleanup.

(ns dev
  (:require [juxt.clip.core :as clip]
            [clojure.tools.namespace.repl :refer [refresh]]))

(def system-config {:a {:start 1}})
(def system nil)

(defn go []
  (alter-var-root #'system (constantly (clip/start system-config))))

(defn stop []
  (alter-var-root #'system
    (fn [s] (when s (clip/stop system-config s)))))

(defn reset []
  (stop)
  (refresh :after 'dev/go))

More Repositories

1

bidi

Bidirectional URI routing
Clojure
991
star
2

aero

A small library for explicit, intentful configuration.
Clojure
743
star
3

yada

A powerful Clojure web library, full HTTP, full async - see https://juxt.pro/yada/index.html
HTML
735
star
4

tick

Time as a value.
Clojure
599
star
5

edge

A Clojure application foundation from JUXT
Clojure
503
star
6

joplin

Flexible datastore migration and seeding for Clojure projects
Clojure
316
star
7

juxt-accounting

Double-entry accounting software written in Clojure with Datomic.
Clojure
281
star
8

pack.alpha

Package clojure projects
Java
258
star
9

mach

A remake of make (in ClojureScript)
Clojure
246
star
10

jig

Jig is an application harness providing a beautifully interactive development experience for Clojure projects.
Clojure
229
star
11

iota

Infix operators for test assertions
Clojure
150
star
12

site

A web and API server, powered by xtdb.com
Clojure
135
star
13

modular

JavaScript
128
star
14

apex

A compendium of Clojure libraries for implementing web backends.
Clojure
123
star
15

bolt

An integrated security system for applications built on component
Clojure
123
star
16

jinx

jinx is not xml-schema (it's json-schema!)
Clojure
99
star
17

pull

Trees from tables
Clojure
96
star
18

roll

AWS Blue/Green deployment using Clojure flavoured devops
Clojure
77
star
19

reap

A Clojure library for decoding and encoding strings used by web protocols.
Clojure
74
star
20

dirwatch

A Clojure directory watcher, wrapping the JDK 7 java.nio.file.WatchService.
Clojure
67
star
21

skip

Skippy McSkipface - A general Clojure dependency tracker
Clojure
34
star
22

snap

Snapshot testing for Clojure and Clojurescript
Clojure
30
star
23

pack-datomic

Datomic Packer and Terraform setup
HCL
26
star
24

qcon2014

JavaScript
26
star
25

stoic

Marrying the Component Lifecycle pattern with distributed config.
Clojure
20
star
26

spin

Unbundling the web application-tier
Clojure
19
star
27

pick

A Clojure library for HTTP server-driven content negotiation.
Clojure
18
star
28

prop

Clojure
17
star
29

grab

A Clojure library for parsing and executing GraphQL queries.
Clojure
16
star
30

rest

A guide to coding a REST service
CSS
15
star
31

chatserver

Clojure
13
star
32

shop

The JUXT Shop - a sample application built on Crux
Clojure
11
star
33

fuzz

Example ClojureScript Project that happens to wrap Slack
Clojure
9
star
34

dotfiles

Recommended dotfiles used by JUXT
Emacs Lisp
9
star
35

ramp

Shell
8
star
36

card

Not sure what this is. A kind-of Zettelkasten?
TypeScript
8
star
37

chatclient

Clojure
8
star
38

hire

A technical interview test for potential employees
Clojure
7
star
39

rock

Hardened AMIs for Clojure deployments (Arch Linux)
Shell
7
star
40

clip-example

Example of using Clip
Clojure
7
star
41

datomic-extras

Clojure
7
star
42

lein-dockerstalk

Clojure
7
star
43

docker

Docker files
Makefile
7
star
44

plain-clojure-lambda

A recipe for building AWS Lambdas using plain clojure
Clojure
7
star
45

draw

A DSL for SVG diagrams
Clojure
6
star
46

trag

Graph transduction
Clojure
5
star
47

radar

JUXT Clojure Technology Radar
Clojure
5
star
48

pack-ek

HCL
4
star
49

adoc

A Clojure wrapper for asciidoctorj
Clojure
4
star
50

msf

Volunteer work for MSF
Clojure
4
star
51

astro-website

Source for the main JUXT website
JavaScript
3
star
52

vext

Clojure
3
star
53

azondi

MQTT goes reactive
CSS
3
star
54

pack-riemann

HCL
3
star
55

yada.cookbook

A cookbook of yada recipes
3
star
56

component-utils

Clojure
2
star
57

blip

Small library for fetching/injecting graphql definitions
Clojure
2
star
58

ping

Clojure
2
star
59

modular.co-dependency

Co-dependency support for com.stuartsierra.component
Clojure
2
star
60

http

2
star
61

lein-deploy-tar

A Leiningen plugin to upload tars to a Maven repository.
Clojure
2
star
62

aleph-issue

Testing aleph
Clojure
1
star
63

mksmarthack

A Clojure data hack on MK:Smart
Clojure
1
star
64

eventing-examples

Clojure
1
star
65

flow

Clojure
1
star
66

training-exercises-webapp

Training exercises
Clojure
1
star
67

example-code

Example code for a recent client course
Clojure
1
star
68

home-apps

Monorepo for SPAs that use home.juxt.site as a backend
TypeScript
1
star
69

sail

Clojure
1
star
70

clojars-mirrors

Re-releasing Clojars libraries onto Maven Central
Clojure
1
star
71

XT20.conf

Public materials for XT20
1
star
72

asciidoctor-stylesheet-factory

CSS
1
star