Code Monkey home page Code Monkey logo

integrant's Introduction

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.

integrant's People

Contributors

borkdude avatar djebbz avatar dsteurer avatar iku000888 avatar kwrooijen avatar oliverm avatar plexus avatar r6eve avatar rickmoynihan avatar teodorlu avatar weavejester avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

integrant's Issues

Integrant elides Midje metaconstants from config

(First of all, I'm loving Integrant! It's changed the way I look at application architecture fundamentally. Thank you for all of your hard work!)

My use case is that I test the initialization and halting of my Integrant keys using config containing Midje metaconstants.

The below:

(require '[integrant.core :as ig]
         '[midje.sweet :refer :all])

(defmethod ig/init-key :some/component
  [_ config]
  (println config))

(fact
  (ig/init {:some/component {:config-key ..value..}})

  => {:some/component {:config-key ..value..}})

Produces:

{:config-key {}}

FAIL at (*cider-repl some-namespace*:479)
    Expected:
{:some/component {:config-key ..value..}}
      Actual:
{:some/component nil}
       Diffs: in [:some/component] expected {:config-key ..value..}, was nil

Component decorator

For example I have following system:

(def config
  {::logger         {}
   ::sms-sender     {}
   ::component-1    {:sms-sender (ig/ref ::sms-sender)}
   ;; ...
   ::component-1000 {:sms-sender (ig/ref ::sms-sender)}})

(defmethod ig/init-key ::logger [_ _]
  #(prn :log %))

(defmethod ig/init-key ::sms-sender [_ _]
  #(prn :sms %))

(defmethod ig/init-key ::component-1 [_ {:keys [sms-sender]}]
  (fn []
    (sms-sender "Hi! I'm 1")))

;; ...

(defmethod ig/init-key ::component-1000 [_ {:keys [sms-sender]}]
  (fn []
    (sms-sender "Hi! I'm 1000")))

And I want to add logged-sms-sendercomponent that logs every sms. I can replace (ig/ref ::sms-sender) with (ig/ref ::logged-sms-sender) 1000 times but I don't want to do this.

Can I decorate a component without changing reference to it?

init breaks on configs with ten or more keys and one reference

I couldn't for the life of me figure out what I'd broken, but eventually as I reduced my config down to almost nothing, I was able to find out where it's breaking. This is kind of a funny one :)

The following code will fail due to NPE.

(do
    (defmethod ig/init-key :a/a1 [_ _])
    (defmethod ig/init-key :a/a2 [_ _])
    (defmethod ig/init-key :a/a3 [_ -])
    (defmethod ig/init-key :a/a4 [_ -])
    (defmethod ig/init-key :a/a5 [_ -])
    (defmethod ig/init-key :a/a6 [_ -])
    (defmethod ig/init-key :a/a7 [_ -])
    (defmethod ig/init-key :a/a8 [_ -])
    (defmethod ig/init-key :a/a9 [_ -])
    (defmethod ig/init-key :a/a10 [_ _])

    (ig/init {:a/a1  {}
              :a/a2  {:_ (ig/ref :a/a1)}
              :a/a3  {}
              :a/a4  {}
              :a/a5  {}
              :a/a6  {}
              :a/a7  {}
              :a/a8  {}
              :a/a9  {}
              :a/a10 {}
              }
             ))

Comment out a single key in the map, and it will work. Or, remove the ig/ref and it will work. It's something to do with resolving references.

Throw an error on not found methods

I was surprised to not get an error on undefined init-key/halt-key! methods for the particular key. Looking at code, this seems to be an intended behaviour: to silently treat keys without methods as plain maps which don't need initialisation/halting logic. What could be the use-case for such feature? I expect most, if not all, keys in the config/system correspond to stateful objects which implement start/stop behaviour.

  1. I'd like to suggest removing :default cases from multimethods to force the user to explicitly specify all the needed methods. This is a less surprising behaviour and helps to early on detect cases when namespace with defined methods for the key was mistakenly not required.

  2. In case the "plain map" pattern must also be supported library could implement the ig/consthelper, e.g.:

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

   ; just a bag of stuff that will be added into system as is
   :bag-of-dependencies (ig/const {:foo 123 :server (ig/ref :adapter/jetty) :handler (ig/ref :handler/greet)}))

pre-init-spec for composite keys

My expectation is that a value under a composite key should somehow conform to the specs returned by pre-init-spec for its component(s).

Using your 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"}}

I'd want to be able to define a pre-init-spec method for :handler/greet and :adapter/jetty:

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

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

But right now this will only work if I define four other specs, one per composite all returning spec(s) defined for the components.

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

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

(defmethod ig/pre-init-spec [:group/a :adapter/jetty] [_]
  (ig/pre-init-spec :adapter/jetty))

(defmethod ig/pre-init-spec [:group/b :adapter/jetty] [_]
  (ig/pre-init-spec :adapter/jetty))

(defmethod ig/pre-init-spec [:group/a :handler/greet] [_]
  (ig/pre-init-spec :handler/greet))

(defmethod ig/pre-init-spec [:group/b :handler/greet] [_]
  (ig/pre-init-spec :handler/greet))

That's a lot of code for what seems like an 80% use-case?

What this allows however is to fully customize the spec for any given composite which might be desirable sometimes I guess.

Maybe a middleground would be to use the spec returned by the composite key if there is one, and if there is none fallback to building an s/and of (map ig/pre-init-spec composite-key) or alternatively use the first spec found, but that leaves the question of which one should be used?

Tips for reloaded workflow

It would be great to have a section about the reloaded REPL workflow in the README because it's one of the main reasons one would want to use Component-like pattern.

In particular, it would be interesting to see if there's a reliable way of reloading multimethods. I've stumbled upon a problem of old definition of method still working after it was removed from ns and reset-ing. To solve this particular problem I've ended with this in a sample project:

(ns user
  "Namespace to support hacking at the REPL.

  Usage:
  o (reset) - [re]run the app.
  o In case REPL fails after syntax error, call (refresh) and try again.
  "
  (:require [my-app.core :as core]
            [integrant.core :as ig]
            [clojure.tools.namespace.repl :refer [refresh refresh-all]]
            [clojure.repl :refer :all]
            [clojure.pprint :refer :all]))

(println "Hi dev!")

(def system (atom nil))

(defn -start
  []
  (reset! system (ig/init core/config)))

(defn -stop
  []
  (swap! system (fn [s] (when s (ig/halt! s)))))

(defn reset
  []
  (-stop)

  ; this will force reloading of the methods
  (remove-all-methods ig/init-key)
  (remove-all-methods ig/halt-key!)
  ; etc.

  ; refresh-all is used instead of refresh because we've just removed all the methods and want to find all the definitions
  (refresh-all :after 'user/-start))

But I'm not sure if refresh-all is going to be fast enough in bigger projects.

halt-missing-keys does not do take dependencies into account

This may be by design, or I may be misunderstanding something, but this behavior seemed pretty counterintuitive, so I figured I'd bring it up.

We have a single "root" key that expands to cover all needed keys, so we start the system like this

(ig/init config [:com.nextjournal/nextjournal])

When doing suspend/resume we do this

(ig/suspend! system)
(ig/resume new-config system [:com.nextjournal/nextjournal])

This however calls halt-key! for (remove #{:com.nextjournal/nextjournal} (keys system)), in other words it halts everything but the root, before resuming, so suspend/resume effectively becomes halt!/init, except for this one key.

We can't do this

(ig/resume new-config system)

because there are keys in there that should not be started.

This could be solved by having suspend! call dependent-keys before computing missing-keys. We will probably do this ourselves as a workaround, even though dependent-keys is private.

(ig/resume new-config system (#'ig/dependent-keys new-config [:com.nextjournal/nextjournal]))

Do you agree that this is an issue? Would you accept a patch for this?

Logo for integrant - suggested designs

If a logo for the Integrant project would be of interest, are any of the designs here meaningful to the maintainers of this project.

https://github.com/practicalli/graphic-design/blob/master/integrant-logo-designs.svg

Adjustments to the designs can be made if any of them are of interest.

Logos are created with the Open Source Inkscape software. If any of these designs are interested they would be donated to the project under a creative commons license, along with original SVG assets.

[Feature] Ability to provide custom hierarchy to `init` method

Consider this scenario:

There is a system described as

(def system-map
 {:adapter/jetty {:port 8080, :handler (ig/ref :handler/greet)}
   :data/store {:jdbc-url "some-yrl}
   :handler/greet {:name "Alice",  :data-store (ig/ref :data/store)}})

For development and production env I want :data/store to be some sql db - with init-key :sql/store.
But for test env I want it to be some mock (which just stores data in atom, for instance) -
with init-key :inmemory/store
(And obviously I don't want to use different system-maps).

It seems natural that I can specify that both keys (:sql/store and :inmemory/store) derived from the :data/store - but this leads to the exception because clojure cant decide which key to use for init-key multimethod.

As a solution, I propose to extend init fn so it accepts local hierarchy, where one can specify how the particular key should be initialised.

For instance, with mentioned system-map it may look like this:

(ig/init system-map (keys system-map) (-> (make-hierarchy) (derive :inmemory/store :data/store))

And during key building integrant just get all descendants of the :data/store and if there is no ambiguity - the descendant will be used.

what do you think?

UPD:

I've started the PR #80.

But I see conflicts with how composite keys now defined (they are also build with derive). Please advise how can I proceed.

Your feedback is much appreciated!

Thanks!

API docs?

Hi, are there API docs hosted anywhere? I couldn't see anything in the README.

Generating documentation for components

The choice to use multi-methods has an annoying side-effect: there's no way to inform editors/codox "this keyword corresponds (among other things) to an extension of integrant.core/init-key". I'm opening this issue to discuss ways in which integrant could (or shouldn't) integrate into tools like codox (and cljdoc) and even orchard/cider & family. I do this with the knowledge that you're also the maintainer of codox, so overlapping thoughts can be brought in.

I experimented with some ad-hoc methods of attaching documentation to multi-method implementations, but they unfortunately failed. My approach was attempting to attach metadata to the functions that go into the multimethod registry itself, and then reading the metadata back out later.

The major feature I'd want is for Codox to be able to detect which libraries are adding the multi-method extensions and show a line-item in the HTML. I think it's worth having an eye to making it reasonably easy for editor tooling to extend integration to whatever we come up with also, but I think the use-case is more constrained there.

`ig/halt!` halting not initialized keys ?

I like very much the fact we can provide keys to ig/init. When in dev, it is useful to be able to work only with a few keys at a time. Nonetheless, it looks like dependent keys (children) of initialized dependencies (parents) are always halted when calling ig/halt!, even if those dependent keys have not been initialized in the first place.

If ::b needs ::a and I only init ::a, ::b will be later halted as well.

Here is a simple gist showing that behavior.

Is it on purpose ? If so, I can't understand why as I find it very inconvenient for my use cases.

Keeping init-key args around for a later halt-key! should be an implementation detail

Hi @weavejester,

As discussed on Slack, we talked about how integrant's halt-key! method could have access to the original init-key arguments. It was the following example that spurred the discussion:

(defmethod ig/init-key ::datasource
  [_ {:keys [logger]}]
  (log logger :info ::starting.datasource)
  (hikari/make-datasource ...))

(defmethod ig/halt-key! ::datasource
  [_ datasource]
  (log ??? :info ::closing.datasource)
  (.close datasource))

How to access the logger at the ??? in halt-key? A solution would be to have the following:

(defmethod ig/init-key ::datasource
  [_ {:keys [logger]}]
  (log logger :info ::starting.datasource)
  {:datasource (hikari/make-datasource ...)
   :logger logger})

(defmethod ig/halt-key! ::datasource
  [_ {:keys [datasource logger]}]
  (log logger :info ::closing.datasource)
  (.close datasource))

However, in my opinion this exposes an implementation detail (::datasource wanting to access the logger, so the actual datasource is inside a map) to the "consumers" of this component.

We discussed three possible solutions.

  1. Adapting halt-key's arguments to something like [_ pre-init-value post-init-value]. Upside is, that this is fairly straightforward. Downside is, it's not clear how to do this without breaking backward compatibility. The example however would be changed to the following:
(defmethod ig/halt-key! ::datasource
  [_ {:keys [logger]} datasource]
  (log logger :info ::closing.datasource)
  (.close datasource))
  1. Adding an Unref protocol having an unref function, together with some helper functions. Integrant will call unref if init-key's return value satisfies the Unref protocol, before passing it to other components. For example:
(defrecord Datasource [datasource logger]
  Unref
  (unref [_] datasource))

(defmethod ig/init-key ::datasource
  [_ {:keys [logger]}]
  (log logger :info ::starting.datasource)
  (map->Datasource
    {:datasource (hikari/make-datasource ...)
     :logger logger}))

Or, with some helper function:

(defmethod ig/init-key ::datasource
  [_ {:keys [logger]}]
  (log logger :info ::starting.datasource)
  (ig/expose :datasource
    {:datasource (hikari/make-datasource ...)
     :logger logger}))

Benefits of this approach are backward compatibility and the developer can keep even more values than just the pre-init value.

  1. A workaround for now without changing integrant, by using the ::build metadata on a system.

As I also stated on Slack, I am just getting started with Integrant, so we agreed we would both ponder if this is an actual problem, and if so, what the best approach would be to solve it.

Multiple keys with same init and halt! behavior

It might be useful to have an official way for sharing init and halt! behavior among keys.
One option is to use derive and the hierarchy system for multimethods.
However my impression is that this feature is only rarely used in general and that it might not be the right way to solve this particular issue.
In general it seems useful to be able to specify the init and halt! behavior for a key as part of the config as opposed to somewhere in the code. (I think Duct is doing something similar.)
For example, if the value for a key is

{:methods 
 {:init foo.component/init
  :halt! foo.component/halt!}
 :options
 {,,,}}

the init behavior could be to apply foo.component/init to the (expanded) options, and add foo.component/halt! as meta-data (similar to the current ::build meta-data).
The halt! behavior would be to apply foo.component/halt!.
With this implementation, it would be easy to share init and halt! behavior (one could even use Refs).
This implementation would also support a more concise form

{:methods foo.component/methods
 :options {,,,}}

where the value of foo.component/methods has the appropriate shape.

Could it make sense to integrate this kind of implementation with the current code base?
One option is to change the :default method for init-key such that it checks if the value for the key has this form (and maybe also if it has some metadata tag like ^:custom-methods or so).
A much more drastic change would be to just replace the current multimethods with this kind of implementation. The main downside I can see here is that it is not clear how to achieve the same level of conciseness as with multimethods. However, a potential advantage is that it seems to solve #8 and maybe also #6.

The 0.6.1 on Clojars is not correct

The jar submitted seems not align to the code tagged on github?

;; git clone and lein install locally
user=> (require '[integrant.core :as ig])
nil
user=> (doc integrant.core/load-namespaces)
-------------------------
integrant.core/load-namespaces
([config] [config keys]) ;; <====== correct signature
  Attempt to load the namespaces referenced by the keys in a configuration.
     If a key is namespaced, both the namespace and the namespace concatenated
     with the name will be tried. For example, if a key is :foo.bar/baz, then the
     function will attempt to load the namespaces foo.bar and foo.bar.baz. Upon
     completion, a list of all loaded namespaces will be returned.
nil
user=> exit
Bye for now!

from Clojars

Retrieving integrant/integrant/0.6.1/integrant-0.6.1.pom from clojars
Retrieving integrant/integrant/0.6.1/integrant-0.6.1.jar from clojars
user=> (require '[integrant.core :as ig])
nil
user=> (doc integrant.core/load-namespaces)
-------------------------
integrant.core/load-namespaces
([config])  ;; <====== incorrect signature
  Attempt to load the namespaces referenced by the keys in a configuration.
     If a key is namespaced, both the namespace and the namespace concatenated
     with the name will be tried. For example, if a key is :foo.bar/baz, then the
     function will attempt to load the namespaces foo.bar and foo.bar.baz. Upon
     completion, a list of all loaded namespaces will be returned.
nil

Clarity on handling prod/dev config

So this looks great, but I have a few clarity questions:

pulling in external environment vars

Is prep-key the right place to do this? Specifically for things like JDBC usernames/passwords etc.

separating prod and dev

In production Jetty isn't used as we deliver a WAR file, so at the moment we have a common.clj which bootstraps the "main" system and is called from either a prod.clj or a dev.clj, both of which add environment specific things (e.g. jetty in dev).

Is this pattern still relevant, and I can see how dev and prod namespaces would include the relevant multimethod implementations, but how do I implement "dev has jetty but prod doesn't"?

That's it for now - thanks!

ClojureScript read config.edn parity via macros

I believe we can introduce cljs feature parity with some of the fns that are currently clj-only (basically read-string & load-namespaces) by means of macros, at least to some extent. Example:

foo (a clojurescript project)
├── resources
│   └── foo
│       └── config.edn
└── src
    └── foo
        ├── core.clj
        └── core.cljs
;;; resources/foo/config.edn
{:foo/bar "42"
 :foo/baz #ig/ref :foo/bar}
;;; src/foo/core.clj
(defmacro read-config [path]
  (-> path io/resource slurp ig/read-string))
;;; src/foo/core.cljs
(def config (read-config "foo/config.edn"))
;; {:foo/bar "42"
;;  :foo/baz #integrant.core.Ref{:key :foo/bar}}

If this seems desirable I can make a more concrete proposal and PR.

Consider removing pom.xml from gitignore

Some build tools (tools.deps) need a pom.xml manifest to work from a Git URL. By ignoring it, people using tools.deps are unable to run from an unpublished branch.

RefMap or resolve as protocol method

RefMap would be like RefSet but would resolve to a map {k -> ref} instead a refs set. Knowing the key can be useful (think handler registration).
Alternatively, extend the RefLike protocol with a resolve method and in expand-key postwalk the config via it, this would allow custom RefThings. Sample:

(defprotocol RefLike
  (ref-key [r] "Return the key of the reference.")
  (resolve [r config]))
;; ...
(defn- expand-key [config value]
  (walk/postwalk
   #(cond
      (reflike? %) (resolve % config)
      :else %)
   value))

load-namespaces not parallel-safe

I am using integrant with eftest, and starting systems in my fixtures. Because load-namespaces is not thread safe, this causes sporadic namespace errors with eftest.

I can sync on my side, but I thought it might be worth doing some kind of sync in integrant.

Subsystems or components that take as input the whole system?

The following two patterns keep occurring in my design.

First is the monitoring or reporter functionality which is not strictly part of the system but needs access to the whole system in order to report its state. The way I imagine it currently is to have a nested system, the outer layer contains two components - monitoring and original system as a component. Or, alternatively a special keyword :integrant/system which would indicate that a components accepts the whole system as input (recursive dependency in a sense).

Second pattern is when an application consists of almost identical sub-systems which are loosely connected through some statistics channels. This again calls for some notion of nested systems.

I wonder what are your thoughts on this and if you intend to add some explicit provisions for nested systems in the future. Thanks!

hard to recover from exceptions thrown during init-key

I'm refactoring my app to use integrant instead of mount. So far it's awesome, exactly what I needed. My app starts an Apache Ignite cluster and associated services, so there is a lot of state flying around, and this is by far the most elegant way of handling it that I've discovered.

One problem I have is that if any init-key methods throw an exception, it causes the entire value returned by init to be nil. This makes it hard to gracefully shut down. I actually have two integrant systems starting at different times in my app. If something fails, I need to get a handle on the existing services to tear them down, otherwise my app can hang on shutdown and cause problems in the cluster.

I propose integrant capture the exception thrown by init-key (which it must already be doing) and substitute that for the value that would have otherwise been set for the key. halt! would also need to be modified to skip such keys.

I realize this muddles the ability to detect a failed init by testing for nil, so perhaps this could be a third arity for init that lets the user explicitly opt in for this behavior if they require it. To simplify the detection of startup failures, perhaps the map init returns in this mode could include a key like {:integrant/status :integrant/failed}. That would be your clue to find and log the exceptions, start a shutdown, or even change something and retry.

Happy to hack on this for you, but wanted to get your thoughts before cutting a PR.

Reader tag not defined although ig/ref exists

Using 0.7.0 in the REPL,

test.core> (ig/ref :help)
#integrant.core.Ref{:key :help}
test.core> #ig/ref :help
Syntax error reading source at (REPL:116:1).
No reader function for tag ig/ref

This is my first attempt to use refs so I don't have too much more to offer. The readme examples didn't work so I poked at it this far and filed.

`pre-init-spec` before any initialization at all

In the current workflow, integrant initializes keys one by one, checking each time if the provided data is valid according to pre-init-spec. When the data is not valid, integrant throws and it actually gets messy. For instance, the resources allocated by the previous keys are not released. Sometimes, this means we have to restart the repl.

What I suggest is that integrant validates all data before initializing anything. The specs we write for our system means that if the data is not valid, something will probably fail. It is best to fail fast instead of starting the system, opening ports and stuff', and eventually failing anyway and leaving a big mess.

Dealing with secrets

When integrant fails to initialise, it throws an exception with the whole config as part of the ExceptionInfo. So when I run an app that looks like this:

(defmethod init-key :failing-thing
  [_ config]
  (throw (Exception. "Kerpow!")))

(defn -main
  []
  (init {:failing-thing {:secret "foo"}}))

I get output that includes this line:

Exception in thread "main" clojure.lang.ExceptionInfo: Error on key :failing-thing when building system {:reason :integrant.core/build-threw-exception, :system {}, :function #object[clojure.lang.MultiFn 0x1c33c17b "clojure.lang.MultiFn@1c33c17b"], :key :failing-thing, :value {:secret "foo"}}, compiling:(C:\Users\david.conlin\AppData\Local\Temp\form-init3479267167971658745.clj:1:115)

This isn't ideal, since we'd rather not be logging secret config values in plaintext. Is there some way we can exclude or obfuscate certain values in any logged/exception values of the config? Alternatively, is there a "production" setting that prevents the config being returned on an exception? If not, would these features be useful (happy to help out with a PR), or have we just gone about this completely the wrong way?

Hot reload on save?

How can I make Integrant auto (reset) or "hot reload" server-side pages when I save my *.clj files? Currently using this with Duct.

Decouple top-level keys from configuration type definitions

Currently, the configuration name and type are coupled together. I believe it would be much simpler to dispatch the configuration based on specific key in the configuration map itself. Both defining and referencing configuration would be much simpler.

Currently:

{[:adapter.sql/hikari-cp :db/app] {:adapter "postgresql"
                                   :username "app"
                                   :password "app"
                                   :server-name "localhost"
                                   :port-number 5432
                                   :database-name "app"}

 [:adapter.sql/hikari-cp :db/mono] {:adapter "postgresql"
                                    :username "mono"
                                    :password "mono"
                                    :server-name "localhost"
                                    :port-number 5432
                                    :database-name "mono"}

 [:adapter.migrations.sql/flyway :flyway/app] {:schemas ["app"]
                                               :locations ["/db/migration/app"]
                                               :migrate? true
                                               :db #ig/ref [:adapter.sql/hikari-cp :db/app]}

 [:adapter.migrations.sql/flyway :flyway/mono] {:schemas ["mono"]
                                                :locations ["/db/migration/mono"]
                                                :migrate? true
                                                :db #ig/ref [:adapter.sql/hikari-cp :db/mono]}}

, this could be rewritten as:

{:db/app {:ig/type :adapter.sql/hikari-cp
          :adapter "postgresql"
          :username "app"
          :password "app"
          :server-name "localhost"
          :port-number 5432
          :database-name "app"}

 :db/mono {:ig/type :adapter.sql/hikari-cp
           :adapter "postgresql"
           :username "mono"
           :password "mono"
           :server-name "localhost"
           :port-number 5432
           :database-name "mono"}

 :flyway/app {:ig/type :adapter.migrations.sql/flyway
              :schemas ["app"]
              :locations ["/db/migration/app"]
              :migrate? true
              :db #ig/ref :db/app}

 :flyway/mono {:ig/type :adapter.migrations.sql/flyway
               :schemas ["mono"]
               :locations ["/db/migration/mono"]
               :migrate? true
               :db #ig/ref :db/mono}}

All ig namespaced keys would be reserved for integrant internals and stripped away from the calls to init-key. There could also be :ig/init, :ig/halt, :ig/schema etc keys with symbols to functions or inlined sci code.

With this change, a top-level key could also have just data as values, without any need to be started or stopped. Would make handling options, feature flags etc. simpler (currently, one needs to create an custom init for all keys).

This would be ok, just data to be referenced with aero ref for example:

{:app/config {:http-port 3000
              :use-dummy-login false}}

Integrant2?

Donation box link?

Hello James and dear contributors,

I'd like to show my support to this and other important work that you did/do in a form of monthly donations. I'm naively hoping that this would allow you to devote more time to them. If you accept such things – maybe add a Patreon link or do a github own thing?

And thanks for some of the awesomest stuff out there!

Kind regards,
Ivan

Could load-namespaces be split off into a separate lib?

I've found the strategy employed by your load-namespaces function to be generically useful for similar architectures that rely on multimethods and namespaced keywords.

I'm currently pulling integrant into a project that doesn't do any state management, just to get the functions called by load-namespaces. I had to make a more generic copypasta version for my own use:

(defn load-namespaces [kws]
  (doall (->> kws
              (filter keyword?)
              (mapcat #(conj (ancestors %) %))
              (mapcat #'ig/key->namespaces)
              (distinct)
              (keep #'ig/try-require))))

This works perfectly for me. What do you think about spinning this stuff off into a separate lib (or maybe adding it to medley?)

Initialisation Order

Would you consider an optional initialisation order, or expose the comparator that sorts system initialisation?

I find myself occasionally enforcing an order between unrelated subsystems using a key that is clearly not intended as a functional dependency, e.g.

:license   {}
:server    {:db/connection #ig/ref :jdbc/connection
            :_ #ig/ref :license}

In the example above :license and :server are unrelated, but I require :license to evaluate first.

Access to parameters a component was started with

Very similar to #25. Perhaps also touching on #21.

I'm trying to print out some metadata about the running system in dev mode, namely running http server(s). My approach to this has been to query the integrant-repl.state variables system and config. However, I realized that (obviously) the config hasn't resolved ig/ref at all.

#21 would suggest that one solution is to have a printing-component, which has an #ig/ref to all components in the system (perhaps filled by a function which performs a transformation).

Consider using namespace qualified tagged literals

I appreciate the succinct nature of the namespace free reader tags e.g. #ref but ideally these tags would be namespaced #juxt.aero/ref or perhaps just #aero/ref. clojure.org has this to say:

Reader tags without namespace qualifiers are reserved for Clojure.

And the edn-format spec re-echoes the same sentiment:

Tag symbols without a prefix are reserved by edn for built-ins defined using the tag system.

There may already be a collision here too, as aero also defines #ref. How would one use both aero and integrant together?

Derived Keys Reference Behaviour

Hi there,

I am trying to rely on derived keywords to be able to start the system with or without db connection pooling. The configuration I have implemented is the following:

(def system
{[:system.db/db :system.db/pool] {:jdbc-url (:jdbc-url config)}
 [:system.db/db :system.db/conn] {:jdbc-url (:jdbc-url config)}
 :system.service/service {::http/port (:http-port config) :db (integrant/ref :service.db/db)}})

I also have the respective multimethod implementations for :system.db/pool and :system.db/conn.

However, when I try to start the system with just a set of keys I get an Ambigous key exception:

(integrant/init system [service.db/conn system.service/service])

Shouldn't that work? It seems to me that should work as I am actually specifying what implementation to use. Just a thought.

Thank you

Access to Enter Integrant presentation

Hi @weavejester
Looking forward to learning this microframework and duct but I need (and hopefully others too) as much documentation as possible on understanding it.

Do you have a new location to the Enter Integrant presentation ? or can you make it part of this repo ?

thanks
James (yes.. another one)

exception handling during build

Currently, when an exception is thrown during build, there is no way to access the partially built system.
It would be useful to have access to it for example in order to halt the keys that have already been built before the exception was thrown.
Could we add an exception handler argument to the build function or have it rethrow the exception with the partially built system attached. (I think component is doing the latter.)

ClojureScript support for `read-string`

Problem

Currently I have a setup with an HTTP server that returns EDN. I need to be able to read this from my ClojureScript application. However ig/read-string is currently only available for Clojure. Possibly for the lack of a built-in EDN library?

Solution

We could add tools.reader as a dependency to integrant. That way we can import cljs.tools.reader.edn specifically for ClojureScript. This would really help me since I'm now copy pasting integrant's read-string function in my ClojureScript application.

What do you think of this idea? Do you possibly have a reason to not support ClojureScript?
If you agree to this change then I'd be willing to create a pull request, to save you the hassle.

Managing system restarts from within the components

Hi,

What is the idiomatic way to manage faults and restarts from within the system? Let's say I have a web socket and a couple of components that depend on that socket:

(def config
  {:feed/db {:name :blabla
             :foo (atom nil)
             :bar (atom nil)}}
  :feed/ws {:url "wss:/ws-feed.example.com"}
  :periodic/ping {:feed (ig/ref :feed/ws)
                  :period 1000
                  :db (ig/ref :feed/db)}
  :periodic/balances {:feed (ig/ref :feed/ws)
                      :period 60000
                      :db (ig/ref :feed/db)})

When web socket :feed/ws breaks for external reasons I would like to automatically restart it and all its dependencies. Thanks.

Idea: Allow distinguishing between private and public state

Sometimes some state is needed in components for efficiency, being able to react to external events, or for being able to properly shut down later. But even though the state is needed, it doesn't necessarily make sense to expose this state to other components. Exposing the additional state would only open opportunities for mistakes, and/or make the component clunkier to use.

Example

Let's say I create a component that on start, creates a db object, and and some object that will periodically mutate it. For the private state, I would then hold on to a mutable db object, and a timer-type object (in order to be able to use it in ":halt").

But it would 0 sense to expose these things to other components. The only thing they should care about is an immutable view of the current db.

I talked to a few people in Clojurians, and they pointed out that one can solve the problem by convention or other tricks, and that makes sense. In some cases, it can also be solved by using "higher-order-components" , e.g., duct.scheduler.simple. Thought I'd share this idea anyway, in case, in case you think it makes sense:

;; In integrant, new record
(defrecord State [private-state public-state-fn])

;; Implementation changes...

In client code:

(defmethod ig/init-key ::service [_ _]
  (ig/map->State
    :private-state {:exec-service  (create-exec-service) :datascript-conn (create-conn) } ;; Constructors omitted
    :public-state-fn
    (fn [private-state]
      ;; Returns an immutable view in this case. 
      ;; A function that returns this (requiring 0 arguments) is what would get exposed to other components.
      @(:datascript-conn private-state)))

  (defmethod ig/halt-key! ::service [_ private-state]
    (.stop (:exec-service private-state))
    private-state)

In the init-key, if people don't return this new State record, it would just work like it does now. (That is, private state is always the same as public state, and doesn't require an invocation to access).

Refset not initialized when loading only subset of keys

It appears that #ig/refset references are not initialized when this is the only place the keys are referenced, and you're loading only a subset of the system.

This makes sense: integrant lazily initialized derived keywords, and when the refset is first encountered, there are no derived keywords yet.

Are there any known workarounds for this?

Naming convention for lifecycle functions

Not all the lifecycle functions end with an exclamation mark:

init
halt!
resume
suspend!
init-key
halt-key!
etc.

I'd expect all the lifecycle functions to have ! (because they are all equally side-effectful/unsafe) or none at all (because I know I deal with stateful stuff when ig/ functions are invoked).

load-namespaces barfs on composite keys

My assumption was that load-namespaces should be passed the same configuration map that gets handed to init, but I noticed that it doesn't handle the case when composite keys are used.

I can submit a PR to handle this case, just thought I'd check the expected behavior before doing so.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.