The environment variables a system reads are its configuration contract. Whoever deploys it needs that list: every variable that exists, what each one means, whether it is required, what it defaults to. Kept by hand, the list drifts — a new component adds a variable and the wiki forgets it. Generate it from the code instead.
Two things have to live in the code for that: a description for each variable, and a way to read the whole set back out.
A variable is a key — a string like "DATABASE_URL", or, once
parsed, a keyword like :env.long/PORT
(Environment variables).
The description belongs with it, but neither a string nor a
keyword can carry Clojure metadata. A symbol can — and the
binding, the left-hand side of the deps-map entry, is a symbol.
So the description goes on the binding, under a metadata key of
your own choosing:
(defn db
{::di/kind :component}
[{^{:app.env/doc "Postgres connection string"} url "DATABASE_URL"}]
...)
One consequence follows from this. The description sits on the use site, not on the variable. The same variable read by several components has several bindings, so it can carry several descriptions — the generator collects them all rather than assuming there is one.
di/inspect takes the same arguments as di/start but builds
nothing; it returns the graph the runtime would walk
(Inspect). A component that
comes from a var carries that var under ::di/variable — env
keys, refs and plain values do not — so keeping on ::di/variable
hands you exactly the var-backed components the system reaches:
(->> (di/inspect :app.system/root)
(keep #(-> % :description ::di/variable)))
;; => (#'app.db/db #'app.mailer/mailer ...)
The descriptions are not in that output; they live in the
metadata of the bindings, inside each var's source. So inspect
tells you which vars, and the vars themselves tell you what
they read. defn records its argument lists under :arglists,
and a component's deps map is the first argument of an arglist:
(-> #'db meta :arglists)
;; => ([{url "DATABASE_URL"}])
You do not parse that map by hand. clojure.core/destructure —
what let expands its own bindings with — flattens it into
binding/expression pairs with metadata intact:
(partition 2 (destructure '[{^{:app.env/doc "HTTP port"} port :env.long/PORT
url "DATABASE_URL"
:or {port 8080}}
env]))
;; (... map plumbing, dropped ...)
;; (port (clojure.core/get env :env.long/PORT 8080))
;; (url (clojure.core/get env "DATABASE_URL"))
Each variable is one pair, and the pair holds every field:
:app.env/doc — the
description;get's second argument is the variable — a string is the
name as-is, a parsed keyword names both the variable (PORT)
and its type (:env.long);get's third argument is the :or default; whether it is
there is the difference between optional and required.Pairs whose expression is not a get — the map binding itself,
other components — are not variables; drop them.
Collected across the vars, this gives you a piece of data per
variable: its name, type, whether it is required, its default,
and the set of descriptions written for it. Turning that into a
document is the last step, and it is yours to shape — a markdown
table beside the deployment guide, a section of --help, a config
template; strict about missing descriptions or lenient; checked in
a test or not.
The recipe stops here because the point is upstream: di/inspect
opens the whole graph to your own configuration tooling — see
Inspect for the rest of what it
exposes.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |