weavejester / integrant

Micro-framework for data-driven architecture

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Decouple top-level keys from configuration type definitions

ikitommi opened this issue · comments

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?

Maybe I'm missing something. But wouldn't this mean that all composite keys MUST have maps as their value? Meaning that this wouldn't be possible?
(:duct/const just returning the opts from init-key)

{[:duct/const :config/base-url] "http://localhost:3000"
 :config/something-else {:base-url #ig/ref :config/base-url}}

Edit: Or are keys without :ig/type constants maybe (That's probably what you meant in your last paragraph)?

:ig/type seems to only accept a single key, but a composite keyword can be a vector of more than 2 keys. Meaning that :ig/type should have to accept a vector, possibly be renamed to :ig/types.

In our system we have multiple keys derive from a certain base key, but then grouped with a second, like so:

{[:app/base :app/group1 :item/1] {}
 [:app/base :app/group1 :item/2] {}
 [:app/base :app/group2 :item/3] {}
 [:app/base :app/group2 :item/4] {}
 :collection/one #refset :app/group1
 :collection/two #refset :app/group2}

I can't say I'm a big fan of having to modify the value of the key to make it "integrant ready".
In my eyes the value should be application specific only.

The main problem (I think) is referencing keys from the map, because you can't use a simple get (because you need all the parent keys).
We could create a reader to mark a key as "integrant ready".

{#ig/key [:app/base :item/one] {:a 1}}

The reader:

(defn ig-key [composite-keys]
  ;; Derive from :ig/key to let the system know this has an init-key
  (derive k :ig/key)                                
  ;; Derive from all parent keys in the vector
  (reduce (fn [k c] (derive k c) k) composite-keys) 
  ;; Return the last key, so that it's easy to reference from the map.
  (last composite-keys))                            

This would result in the following config:

{:item/one {:a 1}}

The main difference is that we don't create a composite-keyword, but derive directly on the last key.
Meaning that we are polluting the global hierarchy, but I don't think your setup would be different edit: your solution wouldn't have this problem.

Haven't used duct, but the example could be written as (re-inventing the #ig/ref too):

{:config {:base-url "http://localhost:3000"}
 :config/something-else {:base-url #ig/ref [:config :base-url]}}

or just (there woudn't be a need to limit data to just maps):

{:config/base-url "http://localhost:3000"
 :config/something-else {:base-url #ig/ref :config/base-url}}

, both IMO much simpler that the duct example.

As there would be breaking changes, a top-level key :ig/version could be used to differentiate the old & new syntax:

{:ig/version "2.0"
 :config {:base-url "http://localhost:3000"}
 :config/somethig-else {:base-url #ig/ref [:config :base-url]}}

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.

Can you explain why you think this is the case? You're decoupling the type from the key, but then coupling the type with the value instead.

In terms of syntax it's less concise, and mandates that the value be a map:

{[::a ::b] {:x 1}}
{::b {:ig/type ::a, :x 1}}

And for those cases where you don't care about separating the name from the type, you'd have to write the type twice:

{::a {:x 1}}
{::a {:ig/type ::a, :x 1}}

It also moves more responsibility onto the end developer:

{:duct.logger/timbre {...}}
{:duct/logger {:ig/type :duct.logger/timbre, ...}}

The developer needs to set both the type and key to a specific value. While this makes things somewhat more explicit to read, it also increases opportunity for error.

This also shows that the key and the type are not entirely decoupled, either. The type of a reference depends on the key you reference, and when thinking about either Duct's modules, or Integrant's prep-key, certain types could mandate certain key names.

So, to me this indicates that this solution is more complex. The type is now coupled with both the key and value, rather than just the key.

If we really want to decouple the relationship of keys, we could have something like the duct hierarchy embedded in integrant. A top level key that defined the keyword hierarchy. The downside of this is that you need to add the key in two places.

{:ig/hierarchy
 {:db/app [:adapter.sql/hikari-cp]
  :db/mono [:adapter.sql/hikari-cp]}

 :db/app {: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"}}

Didn't see this on CHANGELOG, so I guess "closed as completed" means "not going to happen"? That's ok, just want to make sure.

Yes, for the reasons I outlined in my last comment. I'll change the close reason to "not planned".

thanks!