Integrant

repository·master·Indexed 23 days ago

https://github.com/weavejester/integrant

A Clojure/ClojureScript micro-framework for building applications using a data-driven architecture. Integrant defines application systems through configuration data (typically EDN) rather than programmatic construction, providing tools for dependency management, system initialization via ig/init-key, cleanup via ig/halt-key!, and development workflows using suspend! and resume.

Tokens
2.9K
Snippets
15
Records
18
Agent score
30%

What's inside integrant

  1. What is Integrant?

    master

    Integrant is a Clojure (and ClojureScript) micro-framework designed for building applications with a data-driven architecture.

    Unlike frameworks like Component or Mount where systems are constructed programmatically via constructor functions, Integrant defines application architecture through a configuration data structure (typically an EDN resource). This allows for more flexible dependency management, as any entity can be dependent on any other entity, with dependencies resolved from the configuration prior to system initialization.

  2. Use composite references for grouping

    master

    A composite reference matches any key that is derived from every value in a provided vector. This is useful for grouping keys in a configuration. While a normal reference #ig/ref matches any single derived key, a composite reference #ig/ref [...] requires the target to satisfy the entire set of derived relationships.

    {
     [: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"}
    }
  3. Use composite keys for configuration shorthand

    master

    If a configuration key contains a vector of keywords, Integrant treats that key as being derived from every keyword in the vector. This is a way to avoid explicit derive calls when you have multiple keys of the same type (e.g., multiple web adapters).

    {
     [: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"}
    }
  4. Use Vars to inject values into configuration

    master

    A var is a placeholder for a value that is provided later in the lifecycle. You define a var in your configuration using the #ig/var syntax. To provide the actual values, use the ig/bind function with a map of values.

    Note: If any vars remain unbound when ig/init is called, an exception will be thrown.

  5. Suspend and resume systems for development

    master

    To rebuild a system without closing connections or terminating threads (useful during development), use suspend! and resume.

    ig/suspend!

    Acts like halt!, but you can customize it using the ig/suspend-key! multimethod to keep certain resources open.

    ig/resume

    Acts like init, but takes the existing suspended system as an argument. You can customize this using the ig/resume-key multimethod to reuse open resources from the suspended system.

    Note: suspend-key! and halt-key! should be side-effectful and idempotent.

    (ig/suspend! system)
    (def new-system (ig/resume config system))
  6. Expand configurations using expand-key

    master

    Expansions allow you to group multiple keys into a single reusable module. An expansion is defined via the ig/expand-key multimethod. When ig/expand is called, it runs expand-key on every matching key and deep-merges the results into the configuration.

    Priority

    Values explicitly set in the top-level configuration have higher priority than values generated by expansions. This allows you to override expansion defaults.

    Usage

    To use expansions, you must call ig/expand before ig/init:

    (-> config ig/expand ig/init)

    Replacing prep

    expand-key replaces the deprecated prep and prep-key. An expansion method takes the key and value: (defmethod ig/expand-key ::example [k v] ...).

    (-> config ig/expand ig/init)
  7. Reference keys with #ig/ref and #ig/refset

    master

    Integrant provides two ways to reference other keys in the configuration:

    1. #ig/ref: Replaces the reference with the initialized value of the target key. If the target is a parent of multiple derived keys, the reference must be specific enough to be unambiguous.
    2. #ig/refset: Instead of a single value, a refset produces a set of all values matching the specified hierarchy/key.
    ;; Using a refset to collect all matching values
    {:handler/greet-all {:names #ig/refset :const/name}
     :const.name/alice  {:name "Alice"}
     :const.name/bob    {:name "Bob"}}
  8. Configure Integrant with references

    master

    Integrant uses a configuration map where top-level keys represent components to be initialized. Components can reference other components using the ig/ref function (in Clojure) or the #ig/ref reader macro (in EDN). This allows for dependency management within the configuration.

    In Clojure:

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

    In EDN:

    {:adapter/jetty {:port 8080, :handler #ig/ref :handler/greet}
     :handler/greet {:name "Alice"}}
    (def config
      {:adapter/jetty {:port 8080, :handler (ig/ref :handler/greet)}
       :handler/greet {:name "Alice"}})
  9. Manage environments with Profiles

    master

    Profiles allow you to change configuration values based on the environment (e.g., dev vs prod).

    Defining Profiles

    Use the #ig/profile reader macro in your configuration to specify different values for different profile keys:

    {:adapter/jetty {:port #ig/profile {:dev 8080, :prod 80}}}

    Applying Profiles

    Use ig/deprofile to select a profile. It takes the configuration and an ordered collection of profile keys. It returns a new configuration where the first matching profile is applied.

    (ig/deprofile config [:dev])

    Profiles in Expansions

    To ensure profiles are applied correctly within expansions, pass the deprofile function to ig/expand:

    (ig/expand config (ig/deprofile [:dev]))
    (ig/deprofile config [:dev])
  10. Use initializer functions for simple in-memory components

    master

    If a component is entirely in-memory and relies on the garbage collector for cleanup, you can skip defining ig/init-key. Instead, define a function in the same namespace and with the same name as the configuration key. Integrant will automatically find and use it.

    (def config
      {::sugared-greet {:name "Alice"}})
    
    (defn sugared-greet [{:keys [name]}]
      (println "Hi" name))
    
    (ig/init config)
    (def config
      {::sugared-greet {:name "Alice"}})
    
    (defn sugared-greet [{:keys [name]}]
      (println "Hi" name))
    
    (ig/init config)
  11. Initialize and halt components using init-key and halt-key!

    master

    To implement a configuration, you must define how to initialize and (optionly) halt each key using the ig/init-key and ig/halt-key! multimethods.

    ig/init-key

    Takes a key and its configuration value. It is called recursively; keys are initialized in dependency order. The return value of init-key replaces the raw configuration in the system map.

    ig/halt-key!

    Takes a key and its initialized value. It is used for cleanup. Note: halt-key! must be idempotent.

    Managing the system

    • ig/init: Starts the entire configuration in dependency order. Returns the initialized system map.
    • ig/halt!: Shuts down the system in reverse dependency order. This function is side-effectful; ignore its return value.

    Both init and halt! can take an optional collection of keys to limit the scope of the operation to specific components and their dependencies.

    ;; Example implementation
    (defmethod ig/init-key :handler/greet [_ {:keys [name]}]
      (fn [_] (resp/response (str "Hello " name))))
    
    (defmethod ig/halt-key! :adapter/jetty [_ server]
      (.stop server))
    
    ;; Usage
    (def system (ig/init config))
    (ig/halt! system)
    (defmethod ig/init-key :handler/greet [_ {:keys [name]}]
      (fn [_] (resp/response (str "Hello " name))))
    
    (defmethod ig/halt-key! :adapter/jetty [_ server]
      (.stop server))
    
    ;; Usage
    (def system (ig/init config))
    (ig/halt! system)
  12. Install Integrant

    master

    To use Integrant in your Clojure or ClojureScript project, add it as a dependency using your preferred build tool.

    For deps.edn:

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

    For Leiningen:

    [integrant "1.0.1"]
    integrant/integrant {:mvn/version "1.0.1"}