Configuration and Components

Our docs are on GitHub, and all contributions are welcome. Suggest an edit or report a problem.

Edge comes with a few opinions. It does this in order to provide the rest of Edge a mechanism for gluing together other parts automatically.

The main libraries which Edge has a hard dependency on are:

This guide will teach you:

  • The basics of Aero

  • The basics of Integrant

  • How Integrant is integrated into Edge

It is useful to read their respective documentation, but the basics will be covered in this topical guide.

Aero

Edge requires that your Aero configuration be named config.edn and is usually placed at src/config.edn. (but can be placed anywhere at the root of the classpath). This file is a edn file with some tagged elements defined.

Tagged elements take one form after them, so in: #example 100 the example reader is called with 100. This is semantically the same as (example 100) in clojure. A more complex example is #example [100 "foo"], where example is called with [100 "foo"] ((example [100 "foo"])). Tagged elements can take anything valid in edn, including vectors, maps, strings, numbers, symbols. Tagged elements also nest, so you can do #example [100 #example [20 "bar"]] and that’s equivalent to (example [100 (example 20 "bar")]).

Aero provides tagged elements for:

  • Switching between dev, production and other profiles

  • Referencing other information

  • Reading environment variables and other external information

Example 1. config.edn using #profile

Here’s a config.edn making use of #profile.

{:ig/system
 {:edge.yada.ig/listener
  {:port #profile {:dev 4000
                   :prod 3000}}}

#profile takes a map with keywords in. If the map contains the currently active profile (in development Edge sets this to :dev, and in production to :prod) then that value will be chosen.

The end value of this config when read in :dev profile is:

{:ig/system
 {:edge.yada.ig/listener
  {:port 4000}}
Example 2. config.edn using #ref

#ref allows the config to refer to a variable defined elsewhere. This is useful for de-duplicating repeated configuration.

{:web-port 4000
 :ig/system
 {:edge.yada.ig/listener
  {:port #ref [:web-port]}}

Aero will do a get-in of the top-level to find the key. The above is semantically equivalent to:

(let [config {:web-port 4000}]
 (assoc-in config
           [:ig/system :edge.yada.ig/listener :port]
           (get-in config [:web-port])))

Finally, note that you can #ref to values which are tagged elements:

{:web-port #profile {:dev 4000
                     :prod 3000}
 :ig/system
 {:edge.yada.ig/listener
  {:port #ref [:web-port]}}
Example 3. config.edn using #ref

You may find that you need to read some information at runtime, such as the hostname or the port number for your application. Aero provides the #env tag for this use-case.

{:identity #env HOST}

Will read in the $HOST environment variable and replace it. If $HOST is emerald then this is what your config.edn will read as:

{:identity "emeralds"}

If you’re working with a port number, it’s useful to chain #env with #long:

{:web-port #long #env PORT}

This will first read as {:web-port #long "4000"}, and then to {:web-port 4000}.

There are many other useful tags in Aero, so we recommend you read the documentation if you find yourself needing more.

Integrant

Integrant uses the concept of a "system"[1]. A system starts as a configuration map of keys and their configuration. In Edge, your system configuration is under the :ig/system key in config.edn.

In the default template :ig/system is a merge of two other keys. The following documentation uses :ig/system as it’s canonical, but you likely want to update :ig.system/base in your application.

Example 4. config.edn with simple system
{:ig/system
 {:adapter/jetty {:port 8080}}}

In this system, integrant know’s how to init :adapter/jetty. In this case, that would start a jetty web server on port 8080.

Within a system, certain keys will depend on other keys, to support this you can use the #ig/ref tag. Unfortunately, this tag is deceptively similar to Aero’s #ref, but has the following differences:

  • #ig/ref is like get, but #ref is like get-in, so you use #ig/ref :foo

  • #ig/ref is within the system only, it can’t refer to the outer config.edn

Example 5. config.edn using #ig/ref tag
{:ig/system
 {:adapter/jetty {:port 8080, :handler #ig/ref :handler/greet}
  :handler/greet {:name "Alice"}}}

The use of #ig/ref in :adapter/jetty results in :handler/greet being started first, and the result being available to :adapter/jetty when it is started second.

(let [greet (init (handler/greet {:name "Alice"}))]
  {:handler/greet greet
   :adapter/jetty (init (adapter/jetty {:port 8080 :handler greet}))})

In Integrant, you can define how init should behave by extending a multi-method.

Example 6. Extending ig/init-key
(ns com.example.db
  (:require
    [integrant.core :as ig]))

(defn connect
  [uri]
  …)

(defmethod ig/init-key ::conn
  [_ {:keys [uri]}]
  (connect uri))

This will define a new init for :com.example.db/conn where it expects to take some configuration like {:uri "db://localhost:8080"}. It would look like this in config.edn:

{:ig/system
 {:com.example.db/conn {:uri "db://localhost:8080"}}}

When defining a init, you must use namespaced keywords. This allows Edge to automatically load the required namespaces. {:ig/system {:foo.component/bar {:message "hello"}}} will attempt to load foo.component and also foo.component.bar, missing namespaces are ignored.

Keys can also be vectors, this is a strategy for allowing duplicates. In Integrant this is called a composite key.

Example 7. Using a composite key
{:ig/system
 {[:db/conn :com.example.db/users] {:uri "db://localhost:8080"}
  [:db/conn :com.example.db/orders] {:uri "db://localhost:8081"}}}

:db/conn is the actual component (there is a (defmethod ig/init-key :db/conn) somewhere). But in order to allow connections (one for users, one for orders) we give it a second keyword to use.

When referencing a composite key, you should choose the non-generic name.

Example 8. Referencing a composite key
{:ig/system
 {[:db/conn :com.example.db/users] {:uri "db://localhost:8080"}
  [:db/conn :com.example.db/orders] {:uri "db://localhost:8081"}
  :my/handler {:db #ig/ref :com.example.db/users}}}

[:db/conn :com.example.db/users] is the key that will be passed into :my/handler, but integrant knows how to resolve just the specific part.

Conclusion

You should now have a good grasp of the system in Edge.


1. Much like component if you are familiar that
Last updated 2019-05-02 08:54:33 +0100