IntegrantとIntegrant-REPLで:duct/constの機能を実装してみた


こんにちは、@na4daです。

Clojureのライフサイクル管理ライブラリ[Duct](https://github.com/duct-framework/duct)には、config内で定数に名前を与えることができる[`duct/const`](https://github.com/duct-framework/core/blob/c5fb68ac3539248942c50cdbba5a2ca63735ee3c/src/duct/core.clj#L245)があります。その機能を[Integrant](https://github.com/weavejester/integrant)と[Integrant-REPL](https://github.com/weavejester/integrant-repl)で実装する機会があったため、そのメモになります。忘備録的な内容のため、具体的な実装は省いています。

## `duct/const`の使い方

Ductを用いる際に、configファイル(EDN形式)の中で

```clojure
{...
 [:duct/const :some.example/foo]
 "path/to/foo.txt"
 [:duct/const :some.example/bar]
 "path/to/bar.txt"
 ...}
```

このように記述することで、以下のようにconfigファイルの別の場所から`:duct/const`で与えたキー名で参照することができます。

```clojure
{...
 :some.ns/load-files
 {:foo #ig/ref :some.example/foo
  :bar #ig/ref :some.example/bar}
 ...}
```

これらが読み込まれて正常に解釈されると、初期化時にメソッド`:some.ns/load-files`へ渡される実際のデータが以下のようになります。

```clojure
[:some.ns/load-files {:foo "path/to/foo.txt" :bar "path/to/bar.txt"}]
```

## 手順

任意のnamespaceにDuctで導入されている`duct/const`の中身をそのまま記述します。ディスパッチ値は、namespace付きの任意のものを与えます。

```clojure
;; namespace: some.ns

;;; const
;; ::const => :some.ns/const
(defmethod integrant.core/init-key ::const [_ v] v)
```

任意のディスパッチ値をキーとして使い、configを記述します。

```clojure
{...
 [: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の初期化を行うコードです。

```clojure
(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`で作成されるテンプレートで用いられている書き方になります([参考](https://github.com/duct-framework/duct/blob/10d0c6fa38dd2a12614930e5aff5053f8c6c4850/lein-template/resources/leiningen/new/duct/base/dev.clj#L31))。また、`load-config`には`duct/prep-config`内でも用いられている[`load-namespaces`](https://github.com/weavejester/integrant/blob/9c69a440ebff697336efef7e286d62df1cd3f49d/src/integrant/core.cljc#L174-L188)を適用しています。

このように初期化することで、config内では以下のようにして参照することができます。

```clojure
{...
 [: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で用いるキーをディスパッチ値と別のものにしたい場合、例えば


```clojure
{...
 [: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で文字列置換を行います。

```clojure
(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`を使うケースが大半だと思います。使いどころがあまり無いかもしれませんが、もし参考になりましたら幸いです。

## 参考

- [ClojureサーバサイドフレームワークDuctガイド](https://qiita.com/lagenorhynque/items/8201c116c87b40eb9c22)