integrant

arnout 2017-08-23T06:17:54.000084Z

I'm just getting started with integrant, so this may be a silly question. Say I have the following:

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

(defmethod ig/halt-key! ::datasource
  [_ datasource]
  (log/info ??? "Closing datasource...")
  (.close datasource))
As you can see, I would also like to use the logger component when halting the datasource, but where to get it from? Is my only option to put the datasource object in a map, together with a :logger key or something?

2017-08-23T06:43:20.000055Z

I think this is the example similar to your needs: https://github.com/duct-framework/server.http.jetty/blob/99e4cb0c87f587ff38525f8547b2a61a993f4831/src/duct/server/http/jetty.clj I don’t know if it is the only option though.

2017-08-23T06:46:21.000129Z

@arnout @jahson Since that code is written by the guy who write integrant, it's probably a pretty idiomatic way to do it :)

arnout 2017-08-23T06:51:45.000016Z

Hmm, using a map indeed..

arnout 2017-08-23T06:51:57.000087Z

Thanks for the example

arnout 2017-08-23T06:55:20.000113Z

Maybe halt-key! could/should take 3 arguments: the key, the referenced values and its own value?

2017-08-23T07:43:40.000303Z

As far as I understand integrant is a way to transform a config to a running system. Special handling of referenced values will probably add unnecessary complexity. I think @weavejester will explain much better.

arnout 2017-08-23T09:07:38.000437Z

Thing is, every other component that uses the datasource component, must now know that it has to fetch it from a map using some key, instead of using it directly. This use of maps (or records) is a bit like Stuart's Component library. How would repeating the values you receive at init-key in the halt-key! function add unnecessary complexity?

2017-08-23T09:18:04.000426Z

https://github.com/weavejester/integrant#configurations There is almost no need for a component to know a key, you can use #ig/ref to reference component you need. You still need to know the key to get it from init-key second argument, but is more like public contract to me.

2017-08-23T09:23:38.000311Z

The keys in config will be replaced with results of running each init-key, so when the halt-key! is running it is operating not on config, but on system.

arnout 2017-08-23T09:23:50.000310Z

With this I agree > You still need to know the key to get it from init-key second argument, but is more like public contract to me. But, in case of the datasource example above, the other components need to know two keys: its own reference (which is fine indeed), and an extra, more implementation specific key to actually get to the datasource object.

arnout 2017-08-23T09:24:34.000316Z

> The keys in config will be replaced with results of running each init-key, so when the halt-key! is running it is operating not on config, but on system. True, but I don't have access to that system in halt-key!, exactly my "issue".

2017-08-23T09:24:47.000224Z

The system has some meta associated with it though.

2017-08-23T09:30:53.000330Z

And probably integrant can provide refs as the third argument for example, but for some reasons it has gone other way. I think the author can explain why.

arnout 2017-08-23T09:32:03.000093Z

> I think the author can explain why. Sure, when @weavejester comes online I am curious about his answer. Thank you anyway for your insights, @jahson!

2017-08-23T09:33:59.000252Z

AFAIK you always need some kind of extra to do IOC, be it some @Annotation or interface or other things.

2017-08-23T09:35:01.000433Z

Also you can use the most generic key, but provide your own specific implementation.

arnout 2017-08-23T09:58:16.000148Z

Sure, the extra thing is the #ref or (ig/ref ...) in the case of integrant.

2017-08-23T11:40:11.000243Z

Yeah, it helps with decision of what to init first.

2017-08-23T13:16:25.000061Z

@arnout Thanks for the question. When it comes to data sources, my approach is to always wrap them in a “boundary” record:

(defrecord Boundary [datasource])

(defmethod ig/init-key ::datasource [_ {:keys [config logger]}]
  (log/info logger "Starting datasource...")
  (map->Boundary {:datasource (hikari/make-datasource ...), :logger logger}))

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

2017-08-23T13:16:58.000204Z

This allows for a layer of indirection through protocols:

2017-08-23T13:18:09.000018Z

(defprotocol Users
  (get-user [db user-id]))

(extend-protocol Users
  Boundary
  (get-user [{:keys [datasource]} user-id] ...))

2017-08-23T13:19:30.000159Z

This allows for faster testing via database mocks, as functions that take the datasource can now take something that adheres to specific protocols instead.

arnout 2017-08-23T13:21:00.000216Z

Sure, this much I understand. But let's say, I don't want to wrap the component (whatever it is, a datasource, a logger, something else) in a record or map. Because I don't have access to the system on halt-key!, I am still forced to use a record or map.

2017-08-23T13:21:30.000390Z

Right.

2017-08-23T13:22:20.000171Z

I guess we could add in something extra that would carry across extra data without affecting the ref, but it would add complexity.

2017-08-23T13:22:57.000379Z

Effectively you’d need an boundary that’s automatically unwrapped.

arnout 2017-08-23T13:24:05.000119Z

Or allow 3 arguments to halt-key!? I've looked at your source, most ingredients are there; the build function does most of the work, but is only used on init.

2017-08-23T13:24:55.000588Z

@arnout But you’d need two return values from init-key - the bit you want to use as a reference, and the bit you want to pass to halt-key!.

2017-08-23T13:25:11.000137Z

Unless you just use the input to init-key.

arnout 2017-08-23T13:27:24.000033Z

Same input as init-key would be nice I think? E.g. (defmethod halt-key! ::my-thingy [_ pre-init-value post-init-value]).

arnout 2017-08-23T13:28:03.000105Z

(or swap those two arguments, for backwards compatibility)

arnout 2017-08-23T13:28:15.000525Z

Or did you mean something else?

2017-08-23T13:29:18.000286Z

I believe I keep the “pre-init-value” around through ::build metadata for use in resume-key, so that would be possible.

2017-08-23T13:30:18.000115Z

Another solution would be to supply a protocol that “unwraps” the value we want to reference from any extra data.

2017-08-23T13:31:50.000235Z

(defprotocol Unref
  (unref [x]))

(defrecord DataSource [datasource logger]
  Unref
  (unref [_] datasource))

(defmethod ig/init-key ::datasource [_ {:keys [config logger]}]
  (log/info logger "Starting datasource...")
  (->DataSource (hikari/make-datasource ...) logger}))

2017-08-23T13:32:56.000462Z

A protocol would be more flexible, as we could keep extra information not found in the pre-init-value.

2017-08-23T13:33:06.000118Z

And it would also be backward compatible.

2017-08-23T13:33:35.000456Z

Though somewhat harder to use, unless we wrote a helper function:

2017-08-23T13:35:17.000207Z

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

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

arnout 2017-08-23T13:37:18.000154Z

Interesting indeed

arnout 2017-08-23T13:40:38.000372Z

I could use the ::build metadata for now, without changing integrant source, which gives us some time to reflect on this? It's nice to know that there are options, and it seems you acknowledge the value of such an addition?

2017-08-23T13:42:21.000406Z

I don’t think the ::build metadata is directly accessible from halt-key, because it’s on the configuration. However, you could use reverse-run! to execute your own function.

2017-08-23T13:43:50.000457Z

I’ll need to think about this some more, but I tentatively acknowledge the value of it. My thought is that you might want to pass information to halt-key! that is not in the pre-init-value, so I tentatively favour introducing a protocol.

2017-08-23T13:44:57.000151Z

But I think I’d need to see more use-cases to get a feel for what’s the right option.

arnout 2017-08-23T13:45:08.000266Z

I was thinking in the lines of:

(def ^:dynamic *build*)

(defn my-halt [system]
  (binding [*build* (::build system)]
    (ig/halt! system)))

(defn pre-init-value [key]
  (get *build* key))

(defmethod ig/halt-key! ::my-thingy
  [_ value]
  (let [{:keys [logger]} (pre-init-value ::my-thingy)]
    ...))
Ugly maybe, but would work for now...

2017-08-23T13:45:23.000024Z

Ah, yes, that would work.

arnout 2017-08-23T13:46:23.000556Z

Ok, let me know if you have some news regarding this. Or would you like an issue submitted about this at GitHub?

2017-08-23T13:48:14.000380Z

@arnout Please. It’ll mean I don’t forget about it 🙂

2017-08-23T13:49:14.000335Z

It might be a while before I get around to implementing this. I’m trying to be careful with what I add to Integrant, as it’s hard to take away things later on.

2017-08-23T13:51:10.000592Z

And right now I can’t think of a use-case for this where I wouldn’t use a boundary record anyway.

arnout 2017-08-23T14:08:54.000370Z

Ok, I will submit an issue.

arnout 2017-08-23T14:09:17.000003Z

Regarding a use-case, I'm all for using Boundary records, though as per your own recommendation a better database related boundary would be the following?

(defprotocol Users
  (new-user [this username])
  (get-user [this username]))
So the "live" implementation would be
(defrecord UsersImpl [datasource]
  Users
  (new-user [_ username]
    (... use datasource ...))
  ...)
Here, the datasource is more a vehicle for the Users boundary, correct? So not a boundary per se, @weavejester?

2017-08-23T14:34:25.000078Z

@arnout Typically you only want one record per data source, because you can write multiple protocols against it.

2017-08-23T14:34:48.000631Z

So in Duct, I have a duct.database.sql.Boundary record for all SQL databases.

2017-08-23T14:35:26.000681Z

That way I can switch connection pools transparently.

2017-08-23T14:40:07.000042Z

The idea is to wrap any I/O in a protocol, so that it can be substituted for a fake, mock, stub or even another valid data source.

2017-08-23T14:47:32.000721Z

It will look like this

(ns example.boundary.account-options
  (:require [yesql.core :as yesql]
            [duct.database.sql]))

(yesql/defqueries "example/sql/account-options.sql")

(defprotocol AccountOptions
  (find-account-options [this acc-id]))

(extend-protocol AccountOptions
  duct.database.sql.Boundary

  (find-account-options [{:keys [spec]} acc-id]
    (->> {:connection spec}
         (find-by-acc-id {:acc_id acc-id})
         first)))
Don’t bother with yesql, it is old code in rewriting process.