weavejester / integrant

Micro-framework for data-driven architecture
MIT License
1.24k stars 63 forks source link

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

Closed kirillsalykin closed 4 years ago

kirillsalykin commented 4 years ago

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 https://github.com/weavejester/integrant/pull/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!

weavejester commented 4 years ago

It's an interesting idea, but why not just change the configuration to use the test key? The effect will be the same without the need to mess around with the hierarchy.

kirillsalykin commented 4 years ago

It should be changed more than in one place, right? Also as I mentioned - i dont want the system-map to be changed.

weavejester commented 4 years ago

It should be changed more than in one place, right?

What do you mean? You only need to change the keys you want to replace. Everything else, including references, can remain the same.

Also as I mentioned - i dont want the system-map to be changed.

Do you mean the configuration map? The system map is the return value of ig/init.

Also why don't you want it to be changed?

kirillsalykin commented 4 years ago

What do you mean? You only need to change the keys you want to replace. Everything else, including references, can remain the same.

So, if I'll replace :data/store with :sql/store all refs to :data/store will be properly resolved?

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

if so - it may solve my issues.

Do you mean the configuration map? The system map is the return value of ig/init.

Yes, sorry, I meant configuration map.

Also why don't you want it to be changed?

I think you want to use the same configuration map in tests as in prod.

weavejester commented 4 years ago

So, if I'll replace :data/store with :sql/store all refs to :data/store will be properly resolved?

Yes, that's right. A reference like #ig/ref :data/store will look for a key that derives from :data/store.

It's common practice to have both test and production services derive from the same base key, so that you can transparently swap in fake services for testing without altering any other part of the configuration.

I think you want to use the same configuration map in tests as in prod.

Whether you change the keys in the configuration, or change the hierarchy, the resulting system map is the same, and different to production.

My preference is to change the configuration directly, as not only is that simpler (as we don't have to worry about local hierarchies), but it also allows test services to be configured separately to production services.

For example, you might have a fake emailer service that writes to a file, and the filename could be part of the config:

{:fake/emailer {:filename "emails.log"}}

And in production:

{:real/emailer {:smtp "example.com"}}
kirillsalykin commented 4 years ago

Makes sense.

Thanks for clarification!

Closing issue and PR.