Often, especially in production, you don’t know all of the configuration until your application is actually started. For example, in a cloud provider, important IP addresses and port numbers are often assigned dynamically. This information is provided to the processes via environment variables.
This approach has been codified as part of the 12-Factor App manifesto.
However, in most Clojure projects, configuration is done primarily in terms of EDN format configuration files.
This library provides a number of reader macros that allow an EDN configuration file to reference data provided in environment variables, JVM system properties, or elsewhere.
dyn-edn
introduces a little bit of indirection into the otherwise fixed content
of an EDN file, in the form of reader macros.
The dynamic bits are properties:
-
Shell environment variables
-
JVM System properties
-
Explicitly provided properties
The following reader macros are available:
#dyn/prop
-
Accesses dynamic properties. The value is either a key, or a vector of a key and a default value.
#dyn/join
-
Joins a number of values together to form a single string; this is used when building a single string from a mix of properties and static text.
#dyn/long
-
Converts a string to a long value. Typically used with
#dyn/prop
. #dyn/boolean
-
Converts a string to a boolean value. Typically used with
#dyn/prop
. #dyn/keyword
-
Converts a string to a keyword value. Typically used with
#dyn/prop
.
Here’s an example showing all the variants:
{:connection-pool
{:user-name #dyn/prop [DB_USER "accountsuser"]
:user-pw #dyn/prop DB_PW
:url #dyn/join ["jdbc:postgresql://"
#dyn/prop [DB_HOST "localhost"]
":"
#dyn/prop [DB_PORT "5432"]
"/accounts"]}
:web-server
{:port #dyn/long #dyn/prop "WEB_PORT"}}
In this example, the DB_USER
, DB_PW
, DB_HOST
, and DB_PORT
, and WEB_PORT
environment variables
all play a role.
DB_USER
and DB_PORT
are optional, since default values have been provided.
Let’s assume that the DB_HOST
environment variable is db.example.org
,
DB_PW
is change-me
, and WEB_PORT
was 8192
,
and the other referenced environment variables are unset.
After parsing and reader macro expansion, the resulting data will be:
{:connection-pool
{:user-name "accountsuser"
:user-pw "change-me"
:url "jdbc:postgresql://db.example.org:5432/accounts"]}
:web-server
{:port 8192}}
Notice that combining #dyn/long
and #dyn/prop
has ensured that the web server port number is present
as a number, not as a string.
The #dyn/prop
macro’s value is either single key, or a vector of a key and a default value.
The key may be a symbol, keyword, or string.
An exception is thrown if the property is not found and there is no default.
For environment variables or JVM system properties, the dynamic value that replaces the macro will always be a string. For explicit properties, the dynamic value is typically a string, but can be any Clojure data: string, number, keyword, even a map or vector.
Underneath the covers, the key is used for a simple lookup in a map. The map is a merge of all environment variables, all JVM system properties, and any application-provided properties.
As a convenience, each environment variable is added to the map twice: one using a string key, and once with the string key converted to a symbol.
JVM system properties are added using string keys and string values.
Application properties are added exactly as provided; typically this means keyword keys and any kind of data values.
The env-readers
function returns a map of readers; it may optionally be passed additional
properties beyond those obtained from environment variables and JVM system properties.
(require '[clojure.edn :as edn]
'[clojure.java.io :as io]
'[com.walmartlabs.dyn-edn :refer [env-readers])
(->> "config.edn"
io/resource
slurp
(edn/read-string {:readers (env-readers)})