こんにちは、@na4daです。
Clojureのライフサイクル管理ライブラリDuctには、config内で定数に名前を与えることができるduct/const
があります。その機能をIntegrantとIntegrant-REPLで実装する機会があったため、そのメモになります。忘備録的な内容のため、具体的な実装は省いています。
duct/const
の使い方
Ductを用いる際に、configファイル(EDN形式)の中で
{...
[:duct/const :some.example/foo]
"path/to/foo.txt"
[:duct/const :some.example/bar]
"path/to/bar.txt"
...}
このように記述することで、以下のようにconfigファイルの別の場所から:duct/const
で与えたキー名で参照することができます。
{...
:some.ns/load-files
{:foo #ig/ref :some.example/foo
:bar #ig/ref :some.example/bar}
...}
これらが読み込まれて正常に解釈されると、初期化時にメソッド:some.ns/load-files
へ渡される実際のデータが以下のようになります。
[:some.ns/load-files {:foo "path/to/foo.txt" :bar "path/to/bar.txt"}]
手順
任意のnamespaceにDuctで導入されているduct/const
の中身をそのまま記述します。ディスパッチ値は、namespace付きの任意のものを与えます。
;; namespace: some.ns
;;; const
;; ::const => :some.ns/const
(defmethod integrant.core/init-key ::const [_ v] v)
任意のディスパッチ値をキーとして使い、configを記述します。
{...
[:some.ns/const :some.example/foo]
"path/to/foo.txt"
[:some.ns/const :some.example/bar]
"path/to/bar.txt"
...}
configで用いるキーがディスパッチ値に対応していれば、このまま適切に初期化をすることで読み込むことができます。以下は例として、configをconfig.edn
内に記述した状態でのconfigの初期化を行うコードです。
(def ^:private config-file
"path/to/config.edn")
(defn load-config
[config]
(-> config
clojure.java.io/resource
slurp
integrant.core/read-string
(doto integrant.core/load-namespaces)))
(integrant.repl/set-prep! #(load-config config-file))
ここではintegrant.repl/set-prep!
を用います。これはIntegrant-REPLで初期化する際に、下準備として行いたい処理を記述した関数を与えることができる関数です。この記述はもともとlein new duct
で作成されるテンプレートで用いられている書き方になります(参考)。また、load-config
にはduct/prep-config
内でも用いられているload-namespaces
を適用しています。
このように初期化することで、config内では以下のようにして参照することができます。
{...
[:some.ns/const :some.example/foo]
"path/to/foo.txt"
[:some.ns/const :some.example/bar]
"path/to/bar.txt"
...
:another.ns/example-handler
{:foo #ig/ref :some.example/foo
:bar #ig/ref :some.example/bar}
...}
独自の名前をつける
configで用いるキーをディスパッチ値と別のものにしたい場合、例えば
{...
[:def :some.example/foo]
"path/to/foo.txt"
[:def :some.example/bar]
"path/to/bar.txt"
...}
このように、config上では:def
と表記しつつ:some.ns/const
を適用させたい場合は、load-config
の中で、(integrant.core/read-string)
の直後にwalkで文字列置換を行います。
(def ^:private config-file
"path/to/config.edn")
(def ^:private replace-map
{:def :some.ns/def})
(defn load-config
[config]
(let [postwalk-replace* (partial clojure.walk/postwalk-replace replace-map)]
(-> config
clojure.java.io/resource
slurp
integrant.core/read-string
postwalk-replace*
(doto integrant.core/load-namespaces))))
(integrant.repl/set-prep! #(load-config config-file))
ここでは、replace-map
が、ちょうど文字列置換の対応表の役割を担うmapになっています。
終わりに
Clojureでライフサイクル管理を導入した上で手軽に開発を行う場合は、もっぱらIntegrant
の機能を内包したDuct
を使うケースが大半だと思います。使いどころがあまり無いかもしれませんが、もし参考になりましたら幸いです。