Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ The environmentally friendly configuration tool.

## Features:

* Set up environment-specific configuration for your Clojure application, using
the popular YAML file format.
* Set up environment-specific configuration for your Clojure application. Supported file formats include YAML, JSON, and EDN. Supported non-file methods of configuration include native Clojure data, [(IN PROGRESS) system-environment-variables, and command-line options].

* Access config values:

Expand Down Expand Up @@ -55,7 +54,14 @@ the popular YAML file format.

* Optionally auto-load config file.
* using the default filename of "configure.yml" enables autoload
* any other config file name can be specified by calling load-config
* other types of file and non-file data sources can be specified using load-config. Examples:
```clojure
(config/load-config "my-config.yml")

(config/load-config {:src "my-config" :as :yml})

(config/load-config {:src "my-config" :as :edn})
```

* Bind the environment within a calling context using with-env.
```clojure
Expand All @@ -66,10 +72,22 @@ the popular YAML file format.
* Specify the default environment using the MILIEU_ENV system variable.

* Override environment-specific settings using arguments to your command-line
application.
application, or other kinds of sources.
```clojure
(config/commandline-overrides! args)
(config/with-env env
(config/with-env :dev
:overrides {:src args
:as :cli} ; args from -main function
... )

(config/with-env :dev
:overrides {:src {:hello {:world 1}}
:as :data}
... )

(config/with-env :dev
:overrides {:src [[:hello] 2,
[:fou :my-barre] "1.2.3.4"]
:as :assoc-in}
... )
```

Expand Down
7 changes: 4 additions & 3 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
(defproject milieu "0.9.1"
(defproject milieu "1.0.0-SNAPSHOT"
:description "The environmentally friendly configuration tool."
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.4.0"]
[org.clojure/tools.logging "0.2.3"]
[clj-yaml "0.3.1"]
[swiss-arrows "0.3.0"]]
[clj-yaml "0.4.0"]
[cheshire "5.0.1"]
[swiss-arrows "0.5.1"]]
:profiles {:dev {:dependencies [[midje "1.4.0"]]}}
:plugins [[lein-midje "2.0.0-SNAPSHOT"]])
18 changes: 18 additions & 0 deletions resources/configure.example.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{:dev {:fou
{:barre "127.0.0.1"
:car 54}
:size "small"
:smiles [{:mary "8-)"
:fred ":-|"}
{:mary "*_*"
:fred "-__-"}]
:magic false
:skidoo 23}
:test {:fou
{:barre "9.9.9.9", :car 9},
:size "medium",
:magic false},
:prod {:fou
{:barre "7.7.7.7", :car 6},
:size "large",
:magic true}}
25 changes: 25 additions & 0 deletions resources/configure.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{"dev":
{"fou":
{"barre":"127.0.0.1",
"car":54},
"size":"small",
"smiles":[{"mary":"8-)",
"fred":":-|"},
{"mary":"*_*",
"fred":"-__-"}],
"magic":false,
"skidoo":23},

"test":
{"fou":
{"barre":"9.9.9.9",
"car":9},
"size":"medium",
"magic":false},

"prod":
{"fou":
{"barre":"7.7.7.7",
"car":6},
"size":"large",
"magic":true}}
214 changes: 146 additions & 68 deletions src/milieu/config.clj
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
(ns milieu.config
(:require [clojure.java.io :as io]
[clojure.tools.logging :as log]
[clojure.walk :as walk]
[clojure.tools
[logging :as log]
[cli :as cli]]
[clj-yaml.core :as yaml]
[cheshire.core :as json]
[clojure.string :as str]
[swiss-arrows.core :refer [-<>]]))

(defonce ^:private configuration (atom {}))

(defonce ^:private overrides (atom {}))
(def ^{:private false :dynamic true} *overrides* {})

(def ^:private env-sysvar-name "MILIEU_ENV")

Expand All @@ -34,20 +36,149 @@
(or (keyword (getenv env-sysvar-name))
:dev))

(defn ^:private cli [cli-options args]
(let [read-string' (fn [s]
#_"read-string could be used as it is, but it is tiresome
to enter strings like '\"....\"' in command-line args.
To address this, accept tokens verbatim and string-ify
non-keyword, non-number tokens. Tokens that don't play
well with the reader are also interpreted as strings.

As an alternative to this 'type guessing' :cli-options
can be specified, applying clojure.tools.cli."
(if (re-find #"\s" s)
s
(let [x (binding [*read-eval* false]
(try (read-string s) (catch Exception e s)))]
(if (or (keyword? x)
(= (class x) java.lang.Boolean)
(isa? (class x) java.lang.Number))
x
(str s)))))
generate-options (fn [args] (reduce
(fn [r k]
(conj r [k :parse-fn read-string']))
[]
(filter #(= (first %) \-) args)))
cmdarg->cfgkey (fn [s] (-<> s
(str/replace <> #"^-+" "")
(str/split <> #"\.")
(map #(if (re-matches #"\d+" %)
(Integer/parseInt %)
(keyword %))
<>)
vec))
[cmdargs _ banner] (cli/cli args (or cli-options
(generate-options args)))
(reduce-kv (fn [r cmdarg cmdval]
(conj r (cmdarg->cfgkey cmdarg) cmdval))
[]
(apply hash-map args))))

(defn ^:private resolve-format
"if :as format is not specified, determine the format based on source"
[src-spec]
(when src-spec
(or (:as src-spec)
(let [resolve-format*
(fn [src-spec]
(condp re-find src-spec
#"\.ya?ml$" :yml
#"\.json$" :json
#"\.edn$" :edn
(throw (Exception. "Unable to resolve config format."))))]
(if (string? src-spec)
(resolve-format* src-spec)
(resolve-format* (:src src-spec)))))))

(defn ^:private file-src [src-spec process-config]
(let [config-file (io/resource (if (string? src-spec)
src-spec
(:src src-spec)))]
(if-not config-file
(throw (Exception. "config file not found."))
(process-config config-file))))

(defmulti src
":tgt (not yet implemented)
specify a target config environment for these values, for cases where it
is not already specified in the source data itself.

:scope (not yet implemented)
specify a limited scope, such that provided values only apply when the
given scope is indicated at the time of accesing the value.

:as specifies source type.
:yml|:json|:edn
for file-based configuration
:src \"filenamestring\"
:cli
command-line interface
:src [\"--option\" \"value\" ...]
:cli-options (see clojure.tools.cli documentation)
If not specified, then the values are automatically converted in a way
that maintains parity with the conversions applied parsing file formats
:cli-help-function [:help-key (fn [usage-doc] ... )]
optional hook for printing help/usage using tool.cli-generated usage
string.
:data
directly specify values as data.
:src {:dev {:foo 1}}
:assoc-in
directly specify values in the style used by assoc-in.
:src [[:key1 :key1] value, ...]
:environ (system environment vars and lein config)
(not yet implemented - will integrate weavejester/environ functionality)"
resolve-format)

(defmethod src nil [_] {})

(defmethod src :yml [src-spec]
(file-src src-spec #(yaml/parse-string (slurp %))))

(defmethod src :json [src-spec]
(file-src src-spec #(json/parse-string (slurp %) true)))

(defmethod src :edn [src-spec]
(file-src src-spec #(binding [*read-eval* false] (read-string (slurp %)))))

(defmethod src :assoc-in [src-spec]
(reduce-kv assoc-in {} (apply hash-map (:src src-spec))))

(defmethod src :cli [src-spec]
(src (assoc (update-in src-spec [:src] (partial cli (:cli-options src-spec)))
:as :assoc-in)))

(defmethod src :data [src-spec]
(:src src-spec))

(defmacro with-env
"bind the environment to a value for the calling context.

Env can optionally be a vector containing the env and options. Presently
the option :only is supported, which stops execution if the provided
env is not in the limited set."
env is not in the limited set.

The option :overrides provides the ability to override values for the
specified environment

Usage:

(with-env <ENV> ...)
(with-env [<ENV> :only [<ENV1>, <ENV2>, ... ]]
(with-env <ENV> :overrides {:src <DATA> :as <FORMAT>} ...)"
[env & body]
(let [[env {:keys [only] :as options}]
(if (vector? env)
[(first env) (apply hash-map (rest env))]
[env])]
(let [[env {:keys [only] :as options}] (if (vector? env)
[(first env)
(apply hash-map (rest env))]
[env])
[src-spec body] (if (= (first body) :overrides)
[(second body) (next (next body))]
[nil body])]
`(if (and ~only (not ((set ~only) (keyword ~env))))
(throw (Exception. "Access to this environment is prohibited."))
(binding [*env* (or (keyword ~env) *env*)]
(binding [*env* (or (keyword ~env) *env*)
*overrides* (src ~src-spec)]
~@body))))

(defmacro only-env
Expand All @@ -73,8 +204,8 @@

(defn value*
[[k & ks] & optional?]
(let [env-value (get-in @configuration (concat [*env* k] ks))
override-value (get-in @overrides (concat [:cmdargs k] ks))]
(let [env-value (get-in @configuration (concat [*env* k] ks))
override-value (get-in *overrides* (cons k ks))]
(cond (or override-value (false? override-value)) override-value
(or env-value (false? env-value)) env-value
:none-provided (when-not optional?
Expand Down Expand Up @@ -103,64 +234,11 @@
(let [ks (keep identity (flatten [ks alt more]))]
`(value* [~@ks] :optional))))

(defn ^:private keywordize
"helper function for load-config"
[config-map]
(walk/prewalk
(fn [form]
(cond (and (string? form) (= \: (first form)))
(keyword (apply str (rest form))),
(seq? form) (vec form),
:else form))
config-map))

(defn load-config
"load the yaml config file"
[config-name]
(let [config-file (io/resource config-name)]
(if-not config-file (throw (Exception. "config file not found.")))
(swap! configuration
#(merge %
(-> config-file
slurp
yaml/parse-string
keywordize)))))

(defn ^:private commandline-overrides* [args]
(assert (even? (count args)))
(let [index-or-key #(if (re-matches #"\d+" %)
(Integer/parseInt %) (keyword %))
cmdarg->cfgkey
(fn [s] (-<> s
(str/replace <> #"^-+" "")
(str/split <> #"\.")
(map index-or-key <>)
vec))
read-string' (fn [s]
#_"read-string could be used as it is, but it is tiresome
to enter strings like '\"....\"' in command-line args.
To address this, accept tokens verbatim and string-ify
non-keyword, non-number tokens. Tokens that don't play
well with the reader are also interpreted as strings."
(if (re-find #"\s" s)
s
(let [x (binding [*read-eval* false]
(try (read-string s) (catch Exception e s)))]
(if (or (keyword? x)
(= (class x) java.lang.Boolean)
(isa? (class x) java.lang.Number))
x
(str s)))))]
{:cmdargs
(reduce-kv
#(assoc-in %1 (cmdarg->cfgkey %2) (read-string' %3))
{} (apply hash-map args))}))

(defn commandline-overrides!
"override values, regardless of environment.
$ myprogram prod --fou.barre Fred --db.host 127.0.0.1"
[args]
(swap! overrides (fn [m] (merge m (commandline-overrides* args)))))
"load configuration source"
[src-spec]
;; TODO: recursive merge with preexisting config
(swap! configuration #(merge % (src src-spec))))

(when (io/resource default-config-name) ; auto-load if file name convention
(load-config default-config-name)) ; for auto-load is followed.
Loading