Skip to content

Integrant REPLλ︎

Integrant REPL is a library to manage components as part of a REPL workflow, to extend features provided by Integrant.

Integrant REPL includes functions to start, stop and restart services during development, enabling changes to the system without restarting the REPL process.

(start), (reset) and (stop) functions are evaluated in the REPL to control the system.

To assist debugging, (config) displays the parsed system configuration and (system) shows how the configuration has being resolved (service instances, profile values, etc.). Viewing the live system configuration is especially useful when using aero and environment variables to confirm the expected values are used.

User namespaceλ︎

Common practice is to place the Integrant REPl code in a user namespace, which is automatically loaded when the REPL process starts.

The user namespace is defined separately from the source code, as it is code to develop the service rather than part of the service itself. The user namespace is added to the dev/user.clj file and added to the classpath via an alias, e.g. :env/dev or :repl/reloaded aliases from Practicalli Clojure CLI Config

Custom user namespace

(ns user
   ;; REPL workflow
   [integrant.repl       :as ig-repl]
   [integrant.repl.state :as ig-state]

   ;; Environment parsing
   [aero.core :as aero]

   ;; Utilities
   [clojure.pprint :as pprint]))

Practicalli REPL Startup - detailed examples

Practicalli Clojure CLI Config aliases defines aliases that include the dev directory that contains the user namespace on the class path

:repl/reloaded alias starts a rich terminal REPL prompt, with the dev path and several tools to enhance the REPL workflow

clojure -M:repl/reloaded

:dev/reloaded alias adds the dev path and several tools to enhance the REPL workflow

clojure -M:dev/reloaded:repl/rebl

:env/dev alias adds the dev path on REPL start up, include the dev/user.clj file

clojure -M:env/dev:repl/rebl

Environment Configurationλ︎

Using aero with the Integrant configuration file includes tag literals that need to be resolved.

Parsing Aero tags in Integrant system configuration

;;;; Aero environment management

;; extra reader tag for Integrant references
(defmethod aero/reader 'ig/ref
  [_ tag value]
  (ig/ref value))

(defn aero-config
  "Profile specific configuration for all services.
  Profiles supported: :develop :stage :live"
  (aero/read-config (io/resource "config.edn") {:profile profile}))

(defn aero-prep
  "Parse the system config and update values for the given profile (:develop, :stag :live)
  Top-level keys in the config.edn use a qualified name of the Clojure namespace the ig/init-key defmethod is defined in
  ig/load-namespaces will automatically load each namespace referenced by a top-level key in the Integrant configuration
  Return: configuration hash-map for the specified profile (:develop :stage :live)"
  (let [config (aero-config profile)]
    (ig/load-namespaces config)

Parse Configurationλ︎

Parsing Integrant system configuration

(defn integrant-prep!
  "Parse system configuration with aero-reader and apply the given profile values
  Return: Integrant configuration to be used to start the system

  integrant.repl/set-prep! takes an anonymous function that returns an integrant configuration

  Arguments: profile - a keyword determining the environment - :develop :test :stage :live"

   #(aero-prep profile)))

REPL convenience functionsλ︎

REPL convenience functions

(defn go
  "Prepare configuration and start the system services with Integrant-repl"
  ([] (go :develop))
  ([profile] (integrant-prep! profile) (ig-repl/go)))

(defn reset
  "Read updates from the configuration and restart the system services with Integrant-repl"
  ([] (reset :develop))
  ([profile] (integrant-prep! profile) (ig-repl/reset)))

(defn reset-all
  "Read updates from the configuration and restart the system services with Integrant-repl"
  ([] (reset-all :develop))
  ([profile] (integrant-prep! profile) (ig-repl/reset-all)))

(defn stop
  "Shutdown all services"
  [] (ig-repl/halt))

(defn system
  "The running system configuration"
  [] ig-state/system)

(defn config
  "The current system configuration used by Integrant"
  [] ig-state/config)

REPL Commandsλ︎

REPL commands

  ;; Prepare and start the system using the :develop profile or specify the environment
  (go :test)

  ;; Reload changed and new source code files and restart the system
  (reset :develop)

  ;; Reload all source code files on the Classpath and restart the system
  (reset-all :develop)

  ;; Return the current Integrant configuration (already parsed by environment)

  ;; Show the running system configuration, returns nil when system not running

  ;; Shutdown the system using the app-server object reference in the Integrant state

  ;; Pretty print the system state in the REPL
  (pprint/pprint ig-state/system)

  #_()) ;; End of rich comment block
requiring-resolve for Just In Time requires

Integrant in practice provides an example of using requiring-resolve to avoid including all requires in the ns form, potentially reducing REPL startup time by not adding library

When calling an Integrant function, requiring-resolve returns the name of the symbol if already available in the REPL, or requires the functions namespace if the function is not available.

The library containing the namespace must be part of the class path when the REPL starts (or library has been hotloaded into the REPL)

(ns user
  "Reduce REPL startup time by not including requires")

(defmacro jit
  "Resolve a symbol name and require its namespace if not currently available in the REPL"
  `(requiring-resolve '~qualified-symbol))

(defn set-prep! []
  ((jit integrant.repl/set-prep!) #((jit feralberry.system/prep) :dev)))

(defn go []
  ((jit integrant.repl/go)))

(defn reset []
  ((jit integrant.repl/reset)))

(defn system []
  @(jit integrant.repl.state/system))

(defn config []
  @(jit integrant.repl.state/config))