Skip to content

Commit

Permalink
Allow setting nullary functions for :default fields in the configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-yakushev committed Jun 15, 2020
1 parent d1bdcd2 commit 969d5a5
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 16 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

### master (unreleased)
### 0.4.2-SNAPSHOT (unreleased)

- `:default` field for an option can now be a nullary function that is invoked
during the verification phase to generate the actual value based on other
config values. For now, this feature is opt-in to preserve the backwards
compatibility (for hypothetical cases where someone would use Omniconf
defaults to store functions). You need to call
`(cfg/enable-functions-as-defaults)` for this feature to work, but this will
change in the next minor version.

### 0.4.1 (2019-12-06)

Expand Down
30 changes: 27 additions & 3 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
* Omniconf

I [[CHANGELOG.md][https://img.shields.io/badge/-changelog-blue.svg]] I [[https://circleci.com/gh/grammarly/omniconf][https://circleci.com/gh/grammarly/omniconf/tree/master.png]] I [[https://clojars.org/com.grammarly/omniconf][14.4k downloads]] I
I [[CHANGELOG.md][https://img.shields.io/badge/-changelog-blue.svg]] I [[https://circleci.com/gh/grammarly/omniconf][https://circleci.com/gh/grammarly/omniconf/tree/master.png]] I [[https://clojars.org/com.grammarly/omniconf][16.4k downloads]] I

Command-line arguments. Environment variables. Configuration files. Java
properties. Almost every program requires some configuration which is usually
Expand Down Expand Up @@ -178,6 +178,30 @@
value must be specified as a Clojure datatype, not as a string yet to be
parsed.

The value for =:default= can be a nullary function used to generate the
actual default value. This function will be invoked during the verification
phase or on first direct access to the value, whichever happens first;
thus, default functions will have access to other config values provided by
the user. Note that you must invoke =(cfg/enable-functions-as-defaults)=
first for this feature to work. Example:

#+BEGIN_SRC clojure
(cfg/enable-functions-as-defaults)
(cfg/define {:host {:type :string}
:port {:type :number}
:connstring {:type :string
:default #(str (cfg/get :host) ":" (cfg/get :port))}})
(cfg/populate-from-map {:host "localhost", :port 8888})
(cfg/get :connstring) ;; => "localhost:8888"
#+END_SRC

Even if a config option has a functional default, its value can be
explicitly set from any configuration source to a normal value, and in that
case the default function won't be invoked.

Make sure that you don't try to =cfg/get= an option with a function default
value before the values that function depends on are populated.

- =:required= --- if true, the value for this option must be provided,
otherwise =verify= will fail. The value of this parameter can also be a
nullary function: if the function returns true then the option value must
Expand Down Expand Up @@ -332,7 +356,7 @@
doesn't expose an event-based API for this; but it's not too bad since you'd
probably set the interval to 5-10 seconds, so the overhead of polling is not
too big. Also, Omniconf would report setting only the values that actually
has changed.
have changed.

#+BEGIN_SRC clojure
;; Poll values under /prod/myapp/ prefix (and all absolute :ssm-name values too) every 10 seconds.
Expand Down Expand Up @@ -419,7 +443,7 @@

** License

© Copyright 2016-2019 Grammarly, Inc.
© Copyright 2016-2020 Grammarly, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
Expand Down
2 changes: 1 addition & 1 deletion build.boot
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(task-options!
pom {:project 'com.grammarly/omniconf
:version "0.4.1"
:version "0.4.2-SNAPSHOT"
:description "Configuration library for Clojure that favors explicitness"
:license {"Apache License, Version 2.0"
"http://www.apache.org/licenses/LICENSE-2.0"}
Expand Down
46 changes: 35 additions & 11 deletions src/omniconf/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@
"Function that is called to print debugging information and errors."
(atom println))

(def ^:private invoke-default-fns? (atom false))

(defn set-logging-fn
"Change `println` to a custom logging function that Omniconf will use."
[fn]
(reset! logging-fn fn))

(defn enable-functions-as-defaults
"Allow invoking functions passed to :default field for the options."
[]
(reset! invoke-default-fns? true))

(defn- running-in-repl?
"Return true when this function is executed from within the REPL."
[]
Expand Down Expand Up @@ -123,11 +130,17 @@
[& ks]
(let [ks (if (sequential? (first ks)) (first ks) ks)
value (clj/get-in @config-values ks)]
(if (delay? value)
(let [calc-value (force value)]
(swap! config-values assoc-in ks calc-value)
calc-value)
value)))
(cond (delay? value)
(let [calc-value (force value)]
(swap! config-values assoc-in ks calc-value)
calc-value)

(and (fn? value) (::delayed-default (meta value)))
(let [calc-value (value)]
(swap! config-values assoc-in ks calc-value)
calc-value)

:else value)))

(defn set
"Set the `value` for the `ks` path in the current configuration. Path can be
Expand All @@ -146,7 +159,9 @@
(special-action (get ks) value)
value)]
(swap! config-values assoc-in ks (if dt
(delay (dt new-value))
(delay (dt (if (::delayed-default (meta new-value))
(new-value)
new-value)))
new-value))))

(defmacro with-options
Expand All @@ -163,7 +178,10 @@
(let [walk (fn walk [prefix coll]
(doseq [[kw-name spec] coll]
(when-some [default (:default spec)]
(apply set (conj prefix kw-name default)))
(apply set (conj prefix kw-name
(if (and (fn? default) @invoke-default-fns?)
(with-meta default {::delayed-default true})
default))))
(when-let [nested (:nested spec)]
(walk (conj prefix kw-name) nested))))]
(walk [] @config-scheme)))
Expand Down Expand Up @@ -286,8 +304,10 @@
(cond (fn? required) "Conditionally required. "
required "Required. "
:else "")
(when default
(if secret "<SECRET>" default))))))
(cond (nil? default) nil
secret "<SECRET>"
(and (fn? default) @invoke-default-fns?) "<computed>"
:else default)))))

(defn populate-from-cmd
"Fill configuration from command-line arguments."
Expand Down Expand Up @@ -437,8 +457,12 @@ Make sure that com.grammarly/omniconf.ssm dependency is present on classpath."))
(swap! config-scheme dissoc :help) ;; Not needed anymore.
(try-log
(doseq [[kw-name spec] (flatten-and-transpose-scheme :kw @config-scheme)]
(let [value (get-in @config-values kw-name)]
;; Not using `cfg/get` above to avoid forcing delays too early.
(let [value (get-in @config-values kw-name)
;; Not using `cfg/get` above to avoid forcing delays too early. But
;; forcing functional defaults.
value (if (and (fn? value) (::delayed-default (meta value)))
(get kw-name)
value)]
(when-let [r (:required spec)]
(when (and (if (fn? r) (r) r)
(nil? value))
Expand Down
26 changes: 26 additions & 0 deletions test/omniconf/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
[clojure.java.io :as io]
[clojure.test :refer :all]))

(cfg/enable-functions-as-defaults)

(cfg/define
{:help {:description "prints this help message"
:help-name "my-script"
Expand Down Expand Up @@ -66,6 +68,13 @@
:more {:nested {:one {:type :string
:default "one"}
:two {:type :string}}}}}
:nested-default-fn {:nested {:width {:type :number
:default 10}
:height {:type :number
:default 20}
:area {:type :number
:default #(* (cfg/get :nested-default-fn :width)
(cfg/get :nested-default-fn :height))}}}
:delayed-nested {:nested {:delayed {:default "foo"
:delayed-transform #(str % "bar")}}}})

Expand Down Expand Up @@ -225,4 +234,21 @@
(testing "parsing sanity-check"
(is (thrown? Exception (cfg/populate-from-cmd ["--nested-option" "foo"])))
(is (thrown? Exception (cfg/populate-from-cmd ["--integer-option" "garbage"]))))

(testing "default functions"
(is (= {:area 200, :height 20, :width 10} (cfg/get :nested-default-fn)))

(reset! @#'cfg/config-values (sorted-map))
(#'cfg/fill-default-values)
(cfg/populate-from-file "test/omniconf/test-config.edn")
(cfg/populate-from-map {:nested-default-fn {:width 100 :height 200}})
(cfg/verify)
(is (= {:area 20000, :height 200, :width 100} (cfg/get :nested-default-fn)))

(reset! @#'cfg/config-values (sorted-map))
(#'cfg/fill-default-values)
(cfg/populate-from-file "test/omniconf/test-config.edn")
(cfg/populate-from-map {:nested-default-fn {:width 100 :height 200 :area 42}})
(cfg/verify)
(is (= {:area 42, :height 200, :width 100} (cfg/get :nested-default-fn))))
)

0 comments on commit 969d5a5

Please sign in to comment.