• Stars
    star
    1,232
  • Rank 38,102 (Top 0.8 %)
  • Language
    Clojure
  • License
    MIT License
  • Created almost 8 years ago
  • Updated about 2 months ago

Reviews

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

Repository Details

Micro-framework for data-driven architecture

Integrant Build Status

integrant /หˆษชntษชษกr(ษ™)nt/

(of parts) making up or contributing to a whole; constituent.

Integrant is a Clojure (and ClojureScript) micro-framework for building applications with data-driven architecture. It can be thought of as an alternative to Component or Mount, and was inspired by Arachne and through work on Duct.

Rationale

Integrant was built as a reaction to fix some perceived weaknesses with Component.

In Component, systems are created programmatically. Constructor functions are used to build records, which are then assembled into systems.

In Integrant, systems are created from a configuration data structure, typically loaded from an edn resource. The architecture of the application is defined through data, rather than code.

In Component, only records or maps may have dependencies. Anything else you might want to have dependencies, like a function, needs to be wrapped in a record.

In Integrant, anything can be dependent on anything else. The dependencies are resolved from the configuration before it's initialized into a system.

Installation

Add the following dependency to your deps.edn file:

integrant/integrant {:mvn/version "0.8.1"}

Or this to your Leiningen dependencies:

[integrant "0.8.1"]

Presentations

Usage

Configurations

Integrant starts with a configuration map. Each top-level key in the map represents a configuration that can be "initialized" into a concrete implementation. Configurations can reference other keys via the ref (or refset) function.

For example:

(require '[integrant.core :as ig])

(def config
  {:adapter/jetty {:port 8080, :handler (ig/ref :handler/greet)}
   :handler/greet {:name "Alice"}})

Alternatively, you can specify your configuration as pure edn:

{:adapter/jetty {:port 8080, :handler #ig/ref :handler/greet}
 :handler/greet {:name "Alice"}}

And load it with Integrant's version of read-string:

(def config
  (ig/read-string (slurp "config.edn")))

Initializing and halting

Once you have a configuration, Integrant needs to be told how to implement it. The init-key multimethod takes two arguments, a key and its corresponding value, and tells Integrant how to initialize it:

(require '[ring.adapter.jetty :as jetty]
         '[ring.util.response :as resp])

(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}]
  (jetty/run-jetty handler (-> opts (dissoc :handler) (assoc :join? false))))

(defmethod ig/init-key :handler/greet [_ {:keys [name]}]
  (fn [_] (resp/response (str "Hello " name))))

Keys are initialized recursively, with the values in the map being replaced by the return value from init-key.

In the configuration we defined before, :handler/greet will be initialized first, and its value replaced with a handler function. When :adapter/jetty references :handler/greet, it will receive the initialized handler function, rather than the raw configuration.

The halt-key! multimethod tells Integrant how to stop and clean up after a key. Like init-key, it takes two arguments, a key and its corresponding initialized value.

(defmethod ig/halt-key! :adapter/jetty [_ server]
  (.stop server))

Note that we don't need to define a halt-key! for :handler/greet.

Once the multimethods have been defined, we can use the init and halt! functions to handle entire configurations. The init function will start keys in dependency order, and resolve references as it goes:

(def system
  (ig/init config))

When a system needs to be shut down, halt! is used:

(ig/halt! system)

Like Component, halt! shuts down the system in reverse dependency order. Unlike Component, halt! is entirely side-effectful. The return value should be ignored, and the system structure discarded.

It's also important that halt-key! is idempotent. We should be able to run it multiple times on the same key without issue.

Integrant marks functions that are entirely side-effectful with an ending !. You should ignore the return value of any function ending in a !.

Both init and halt! can take a second argument of a collection of keys. If this is supplied, the functions will only initiate or halt the supplied keys (and any referenced keys). For example:

(def system
  (ig/init config [:adapter/jetty]))

Suspending and resuming

During development, we often want to rebuild a system, but not to close open connections or terminate running threads. For this purpose Integrant has the suspend! and resume functions.

The suspend! function acts like halt!:

(ig/suspend! system)

By default this functions the same as halt!, but we can customize the behavior with the suspend-key! multimethod to keep open connections and resources that halt-key! would close.

Like halt-key!, suspend-key! should be both side-effectful and idempotent.

The resume function acts like init but takes an additional argument specifying a suspended system:

(def new-system
  (ig/resume config system))

By default the system argument is ignored and resume functions the same as init, but as with suspend! we can customize the behavior with the resume-key multimethod. If we implement this method, we can reuse open resources from the suspended system.

To illustrate this, let's reimplement the Jetty adapter with the capability to suspend and resume:

(defmethod ig/init-key :adapter/jetty [_ opts]
  (let [handler (atom (delay (:handler opts)))
        options (-> opts (dissoc :handler) (assoc :join? false))]
    {:handler handler
     :server  (jetty/run-jetty (fn [req] (@@handler req)) options)}))

(defmethod ig/halt-key! :adapter/jetty [_ {:keys [server]}]
  (.stop server))

(defmethod ig/suspend-key! :adapter/jetty [_ {:keys [handler]}]
  (reset! handler (promise)))

(defmethod ig/resume-key :adapter/jetty [key opts old-opts old-impl]
  (if (= (dissoc opts :handler) (dissoc old-opts :handler))
    (do (deliver @(:handler old-impl) (:handler opts))
        old-impl)
    (do (ig/halt-key! key old-impl)
        (ig/init-key key opts))))

This example may require some explanation. Instead of passing the handler directly to the web server, we put it in an atom, so that we can change the handler without restarting the server.

We further encase the handler in a delay. This allows us to replace it with a promise when we suspend the server. Because a promise will block until a value is delivered, once suspended the server will accept requests but wait around until it's resumed.

Once we decide to resume the server, we first check to see if the options have changed. If they have, we don't take any chances; better to halt and re-init from scratch. If the server options haven't changed, then deliver the new handler to the promise which unblocks the server.

Note that we only need to go to this additional effort if retaining open resources is useful during development, otherwise we can rely on the default init and halt! behavior. In production, it's always better to terminate and restart.

Like init and halt!, resume and suspend! can be supplied with a collection of keys to narrow down the parts of the configuration that are suspended or resumed.

Resolving

It's sometimes useful to hide information when resolving a reference. In our previous example, we changed the initiation from:

(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}]
  (jetty/run-jetty handler (-> opts (dissoc :handler) (assoc :join? false))))

To:

(defmethod ig/init-key :adapter/jetty [_ opts]
  (let [handler (atom (delay (:handler opts)))
        options (-> opts (dissoc :handler) (assoc :join? false))]
    {:handler handler
     :server  (jetty/run-jetty (fn [req] (@@handler req)) options)}))

This changed the return value from a Jetty server object to a map, so that suspend! and resume would be able to temporarily block the handler. However, this also changes the return type! Ideally, we'd want to pass the handler atom to suspend-key! and resume-key, without affecting how references are resolved in the configuration.

To solve this, we can use resolve-key:

(defmethod ig/resolve-key :adapter/jetty [_ {:keys [server]}]
  server)

Before a reference is resolved, resolve-key is applied. This allows us to cut out information that is only relevant behind the scenes. In this case, we replace the map with the container Jetty server object.

Prepping

Sometimes keys also require some preparation. Perhaps you have a particularly complex set of default values, or perhaps you want to add in default references to other keys. In these cases, the prep-key method can help.

(defmethod ig/prep-key :adapter/jetty [_ config]
  (merge {:port 8080} config))

The prep-key method will change the value of a key before the configuration is initialized. In the previous example, the :port would default to 8080 if not set.

All keys in a configuration can be prepped using the prep function before the init function:

(-> config ig/prep ig/init)

If prep-key is not defined, it defaults to the identity function. Prepping keys is particularly useful when adding default references to derived keywords.

Derived keywords

Keywords have an inherited hierarchy. Integrant takes advantage of this by allowing keywords to refer to their descendants. For example:

(derive :adapter/jetty :adapter/ring)

This sets up a hierarchical relationship, where the specific :adapter/jetty keyword is derived from the more generic :adapter/ring.

We can now use :adapter/ring in place of :adapter/jetty:

(ig/init config [:adapter/ring])

We can also use it as a reference, but only if the reference is unambiguous, and only refers to one key in the configuration.

Composite keys

Sometimes it's useful to have two keys of the same type in your configuration. For example, you may want to run two Ring adapters on different ports.

One way would be to create two new keywords, derived from a common parent:

(derive :example/web-1 :adapter/jetty)
(derive :example/web-2 :adapter/jetty)

You could then write a configuration like:

{:example/web-1 {:port 8080, :handler #ig/ref :handler/greet}
 :example/web-2 {:port 8081, :handler #ig/ref :handler/greet}
 :handler/greet {:name "Alice"}}

However, you could also make use of composite keys. If your configuration contains a key that is a vector of keywords, Integrant treats it as being derived from all the keywords inside it.

So you could also write:

{[:adapter/jetty :example/web-1] {:port 8080, :handler #ig/ref :handler/greet}
 [:adapter/jetty :example/web-2] {:port 8081, :handler #ig/ref :handler/greet}
 :handler/greet {:name "Alice"}}

This syntax sugar allows you to avoid adding extra derive instructions to your source code.

Composite references

Composite references complement composite keys. A normal reference matches any key derived from the value of the reference. A composite reference matches any key derived from every value in a vector.

For example:

{[:group/a :adapter/jetty] {:port 8080, :handler #ig/ref [:group/a :handler/greet]}
 [:group/a :handler/greet] {:name "Alice"}
 [:group/b :adapter/jetty] {:port 8081, :handler #ig/ref [:group/b :handler/greet]}
 [:group/b :handler/greet] {:name "Bob"}}

One use of composite references is to provide a way of grouping keys in a configuration.

Refs vs refsets

An Integrant ref is used to reference another key in the configuration. The ref will be replaced with the initialized value of the key. The ref does not need to refer to an exact key - the parent of a derived key may be specified, so long as the ref is unambiguous.

For example suppose we have a configuration:

{:handler/greet    {:name #ig/ref :const/name}
 :const.name/alice {:name "Alice"}
 :const.name/bob   {:name "Bob"}}

And some definitions:

(defmethod ig/init-key :const/name [_ {:keys [name]}]
  name)

(derive :const.name/alice :const/name)
(derive :const.name/bob   :const/name)

In this case #ig/ref :const/name is ambiguous - it could refer to either :const.name/alice or :const.name/bob. To fix this we could make the reference more specific:

{:handler/greet    {:name #ig/ref :const.name/alice}
 :const.name/alice {:name "Alice"}
 :const.name/bob   {:name "Bob"}}

But suppose we want to greet not just one person, but several. In this case we can use a refset:

{:handler/greet-all {:names #ig/refset :const/name}
 :const.name/alice  {:name "Alice"}
 :const.name/bob    {:name "Bob"}}

When initialized, a refset will produce a set of all matching values.

(defmethod ig/init-key :handler/greet-all [_ {:keys [names]}]
  (fn [_] (resp/response (str "Hello " (clojure.string/join ", " names))))

Specs

It would be incorrect to write specs directly against the keys used by Integrant, as the same key will be used in the configuration, during initiation, and in the resulting system. All will likely have different values.

To resolve this, Integrant has an pre-init-spec multimethod that can be extended to provide Integrant with a spec to test the value after the references are resolved, but before they are initiated. The resulting spec is checked directly before init-key, and an exception is raised if it fails.

Here's how our two example keys would be specced out:

(require '[clojure.spec.alpha :as s])

(s/def ::port pos-int?)
(s/def ::handler fn?)

(defmethod ig/pre-init-spec :adapter/jetty [_]
  (s/keys :req-un [::port ::handler]))

(s/def ::name string?)

(defmethod ig/pre-init-spec :handler/greet [_]
  (s/keys :req-un [::name]))

If we try to init an invalid configuration:

(ig/init {:adapter/jetty {:port 3000} :handler/greet {:name "foo"}})

Then an ExceptionInfo is thrown explaining the error:

ExceptionInfo Spec failed on key :adapter/jetty when building system
val: {:port 3000} fails predicate: (contains? % :handler)

Loading namespaces

It can be hard to remember to load all the namespaces that contain the relevant multimethods. If you name your keys carefully, Integrant can help via the load-namespaces function.

If a key has a namespace, load-namespaces will attempt to load it. It will also try concatenating the name of the key onto the end of its namespace, and loading that as well.

For example:

(load-namespaces {:foo.component/bar {:message "hello"}})

This will attempt to load the namespace foo.component and also foo.component.bar. A list of all successfully loaded namespaces will be returned from the function. Missing namespaces are ignored.

Reloaded workflow

See Integrant-REPL to use Integrant systems at the REPL, in line with Stuart Sierra's reloaded workflow.

Further Documentation

License

Copyright ยฉ 2023 James Reeves

Released under the MIT license.

More Repositories

1

compojure

A concise routing library for Ring/Clojure
Clojure
4,029
star
2

hiccup

Fast library for rendering HTML in Clojure
Clojure
2,571
star
3

cljfmt

A tool for formatting Clojure code
Clojure
1,112
star
4

environ

Library for managing environment variables in Clojure
Clojure
923
star
5

medley

A lightweight library of useful Clojure functions
Clojure
867
star
6

codox

Clojure documentation tool
Clojure
667
star
7

ragtime

Database-independent migration library
Clojure
608
star
8

lein-ring

Ring plugin for Leiningen
Clojure
501
star
9

hashp

A better "prn" for debugging
Clojure
442
star
10

eftest

Fast and pretty Clojure test runner
Clojure
424
star
11

reagi

An FRP library for Clojure and ClojureScript
Clojure
232
star
12

clout

HTTP route-matching library for Clojure
Clojure
230
star
13

ataraxy

A data-driven Ring routing and destructuring library
Clojure
209
star
14

crypto-password

Library for securely hashing passwords
Clojure
204
star
15

clj-aws-s3

S3 client library for Clojure
Clojure
198
star
16

clojure-toolbox.com

Source to clojure-toolbox.com
CSS
179
star
17

reloaded.repl

REPL functions to support the reloaded workflow
Clojure
178
star
18

clucy

Clojure interface to Lucene
Clojure
172
star
19

haslett

A lightweight WebSocket library for ClojureScript
Clojure
172
star
20

integrant-repl

Reloaded workflow functions for Integrant
Clojure
158
star
21

lein-beanstalk

Leiningen plugin for Amazon's Elastic Beanstalk service
Clojure
149
star
22

ring-oauth2

OAuth 2.0 client middleware for Ring
Clojure
144
star
23

brutha

Simple ClojureScript interface to React
Clojure
139
star
24

progrock

A functional Clojure progress bar for the command line
Clojure
134
star
25

lein-auto

A Leiningen plugin that executes tasks when files are modifed
Clojure
132
star
26

ns-tracker

Library to keep track of changes to Clojure source files
Clojure
114
star
27

meta-merge

A standalone implementation of Leiningen's meta-merge function
Clojure
105
star
28

ring-mock

Library to create mock ring requests for unit tests
Clojure
86
star
29

ring-anti-forgery

Ring middleware to prevent CSRF attacks
Clojure
76
star
30

crypto-random

Clojure library for generating cryptographically secure random bytes and strings
Clojure
72
star
31

crouton

HTML parsing library for Clojure
Clojure
68
star
32

comb

Clojure templating library
Clojure
67
star
33

ittyon

Library to manage distributed state for games
Clojure
58
star
34

compojure-example

An example Compojure project
Clojure
57
star
35

hiccup-bootstrap

Twitter's bootstrap in Hiccup
Clojure
57
star
36

lein-generate

Leiningen plugin for generating source file templates
Clojure
54
star
37

euclidean

Fast, immutable math for 3D geometries in Clojure
Clojure
52
star
38

impi

ClojureScript library for using Pixi.js through immutable data
Clojure
51
star
39

ring-server

Clojure
51
star
40

valip

Validations library for Clojure 1.2
Clojure
51
star
41

rotary

DynamoDB API for Clojure
Clojure
47
star
42

flupot

ClojureScript functions for creating React elements
Clojure
45
star
43

re-rand

Clojure library to generate random strings from regular expressions
Clojure
43
star
44

ring-webjars

Ring middleware to serve assets from WebJars
Clojure
36
star
45

ring-refresh

A Clojure middleware library for Ring that automatically triggers a browser refresh
Clojure
33
star
46

abrade

Clojure library for web scraping
Clojure
32
star
47

ring-jetty-component

A component for the standard Ring Jetty adapter
Clojure
32
star
48

tcp-server

Clojure TCP server library
Clojure
32
star
49

intentions

Multimethods that combine rather than override inherited behavior
Clojure
31
star
50

compojure-template

Compojure project template for Leiningen
Clojure
28
star
51

suspendable

A Clojure library to add suspend and resume methods to Component
Clojure
27
star
52

ring-serve

Ring development web server
Clojure
25
star
53

decorate

Clojure macros for decorating functions
Clojure
24
star
54

crypto-equality

A small Clojure library for securely comparing strings or byte arrays
Clojure
24
star
55

resauce

Clojure library for handling JVM resources
Clojure
24
star
56

fact

Unit testing library for Clojure (no longer in active dev)
Clojure
23
star
57

dotfiles

My configuration files
Emacs Lisp
21
star
58

inquest

A library for non-invasive monitoring in Clojure
Clojure
20
star
59

evaljs

Evaluate Javascript code and libraries in Clojure
Clojure
20
star
60

fish-git

Git completions and functions for the Fish Shell
18
star
61

snowball-stemmer

Snowball Stemmer for Clojure
Java
17
star
62

hop

An experimental declarative build tool for Clojure
Clojure
16
star
63

build

Clojure
15
star
64

coercer

Library to convert Clojure data into different types
Clojure
14
star
65

whorl

Generate unique fingerprints for Clojure data structures
Clojure
14
star
66

clojure-over-ajax

Ajax Clojure REPL based on why's Try Ruby
JavaScript
13
star
67

flupot-pixi

A ClojureScript wrapper around react-pixi
Clojure
13
star
68

websocket-example

Small example Ring/Aleph project for demonstrating websockets
Clojure
12
star
69

ring-json-response

Ring responses in JSON
Clojure
11
star
70

crumpets

Clojure library for dealing with color
Clojure
10
star
71

duct-hikaricp-component

Clojure component for managing a HikariCP connection pool
Clojure
10
star
72

clojure-dbm

Clojure interface to key-value databases
Clojure
9
star
73

hanami

A Clojure utility library for Heroku web applications
Clojure
9
star
74

strowger

A ClojureScript library for managing DOM events
Clojure
9
star
75

crypto-keystore

Clojure library for dealing with Java keystores
Clojure
8
star
76

substream

Stream subclassing in Clojure
Clojure
7
star
77

clj-daemon

Clojure daemon to avoid JVM startup time
Clojure
7
star
78

ring-reload-modified

Ring middleware that automatically reloads modifed source files
Clojure
7
star
79

duct-ragtime-component

Clojure component for managing migrations with Ragtime
Clojure
5
star
80

ring-honeybadger

Ring middleware for sending errors to HoneyBadger
Clojure
4
star
81

imprimatur

Data visualization library for ClojureScript and React
Clojure
4
star
82

hassium

Another Clojure MongoDB library
Clojure
4
star
83

contributing

Contributor's Guide
4
star
84

lein-template

Clojure
4
star
85

delegance

A Clojure library for remote evaluation
Clojure
3
star
86

po

A command-line tool for organizing project-specific scripts
Go
3
star
87

lein-version-script

A Leiningen plugin to set the project version from a shell script
Clojure
3
star
88

capra

An extensible package manager for Clojure
Clojure
3
star
89

eclair

Clojure
3
star
90

wrepl

Web-based Clojure REPL
Clojure
2
star
91

pocketses

Personal Wiki template that uses Gollum
CSS
2
star
92

clj-less

LESS interpreter for Clojure (http://lesscss.org)
Clojure
2
star
93

dewdrop

Web UI framework
2
star
94

ubitcoin

Bitcoin GUI for Ubuntu
Python
2
star
95

clojure-sandbox

Miscellaneous Clojure libraries that needed a home
Clojure
2
star
96

capra-server

RESTful package server
Clojure
1
star
97

delegance-aws

Library to integrate Delegance with Amazon Web Services
Clojure
1
star
98

weavejester.github.com

JavaScript
1
star
99

dojo-poetry

Code for Clojure Dojo 2012-08-28
Clojure
1
star
100

databstract

1
star