ClojureサーバサイドフレームワークDuctガイド

ClojureによるWeb開発のサーバサイドで注目度の高い(マイクロ)フレームワークとして[Duct](https://github.com/duct-framework/duct)があります。

ちょうど1年前の2017年12月に私自身初めてDuctによる簡単なREST APIを試作してみて、その過程を[Advent Calendar記事](https://qiita.com/lagenorhynque/items/57d5aa086c4a080a1c54)にしたことがありました。

今回は主にClojure言語のことを知っていてWebアプリ開発に興味のある方向けに、英語も含めてドキュメントのとても少ないDuctというフレームワークがどのようなものなのか、技術的な詳細に踏み込んだ解説を試みます。

ちなみに、執筆時点で参照/動作確認した各種ライブラリのバージョンは、サンプルプロジェクト"hello-duct"の[project.clj](https://github.com/lagenorhynque/hello-duct/blob/master/project.clj)の通りです(Duct最新安定版[0.12.0](https://github.com/duct-framework/duct/tree/0.12.0)のテンプレートのまま)。

# Ductとは

アプリケーション状態/ライフサイクル管理ライブラリのひとつ[Integrant](https://github.com/weavejester/integrant)を基礎としたサーバサイド(マイクロ)フレームワークです[^1]。

GitHubの[duct-framework](https://github.com/duct-framework)を見ると、主に次のような要素で構成されていることが分かります。

- プロジェクト生成ユーティリティ
    - [duct](https://github.com/duct-framework/duct)
        - [duct/lein-template](https://github.com/duct-framework/duct/tree/master/lein-template): Leiningenテンプレート(コマンド `lein new duct ...`)
        - [duct/lein-duct](https://github.com/duct-framework/duct/tree/master/lein-duct): Leiningenプラグイン(コマンド `lein duct ...`)
- コアライブラリ
    - [duct/core](https://github.com/duct-framework/core): コア機能を提供するライブラリ
- Ductモジュール
    - [duct/module.web](https://github.com/duct-framework/module.web): Web/API関連機能を導入するDuctモジュール
    - [duct/module.ataraxy](https://github.com/duct-framework/module.ataraxy): ルーティングライブラリ[Ataraxy](https://github.com/weavejester/ataraxy)を導入するDuctモジュール
    - [duct/module.sql](https://github.com/duct-framework/module.sql): SQL/DBアクセス関連機能を導入するDuctモジュール
    - [duct/module.cljs](https://github.com/duct-framework/module.cljs): ClojureScriptフロントエンド関連機能を導入するDuctモジュール
    - [duct/module.logging](https://github.com/duct-framework/module.logging): ログ出力関連機能を導入するDuctモジュール

以下では、Ductというフレームワークがどのような機能を提供しているのか、また、どのように拡張可能なのか、順に見ていくことにしましょう。

# [duct/lein-template](https://github.com/duct-framework/duct/tree/master/lein-template)

ductリポジトリ配下にあるプロジェクト"duct/lein-template"は名前からも分かるようにLeiningenのテンプレートです。

一般にLeiningenのテンプレート機能はプロジェクトの初期のディレクトリ構成や設定ファイル類を素早く自動生成するためのものであり、本格的にアプリケーションを開発していく際には構成や設定を見直して目的に合わせて適宜手を加えていくことになると思いますが、スタート時点で基本形を構築するのに非常に便利です。

ちなみに、テンプレートから生成された `project.clj` では依存ライブラリが最新版でなかったり、推移的な依存ライブラリが整理されていなかったりすることがあるので、 `lein ancient :all`(cf. [lein-ancient](https://github.com/xsc/lein-ancient)), `lein deps :tree` などで確認して必要に応じて更新しておくのがオススメです。

執筆時点の最新安定版0.12.0の[src/leiningen/new/duct.clj](https://github.com/duct-framework/duct/blob/0.12.0/lein-template/src/leiningen/new/duct.clj)を見てみると、

```clj
(defn duct
  "Create a new Duct web application.

Accepts the following profile hints:
  +api      - adds API middleware and handlers
  +ataraxy  - adds the Ataraxy router
  +cljs     - adds in ClojureScript compilation and hot-loading
  +example  - adds an example handler
  +heroku   - adds configuration for deploying to Heroku
  +postgres - adds a PostgreSQL dependency and database component
  +site     - adds site middleware, a favicon, webjars and more
  +sqlite   - adds a SQLite dependency and database component"
  [name & hints]
  (when (.startsWith name "+")
    (main/abort "Failed to create project: no project name specified."))
  (main/info (str "Generating a new Duct project named " name "..."))
  (generate-project (project-template name hints))
  (main/info "Run 'lein duct setup' in the project directory to create local config files."))
```

という関数 [`duct`](https://github.com/duct-framework/duct/blob/0.12.0/lein-template/src/leiningen/new/duct.clj#L115-L132) が定義されていて、 `lein new duct <プロジェクト名> <プロファイル>*` という形式でDuctのプロジェクトのscaffoldingを生成することができます[^2]。

例えば

```sh
$ lein new duct hello-duct +api +ataraxy +example
Generating a new Duct project named hello-duct...
Run 'lein duct setup' in the project directory to create local config files.
```

とすると、API関連、ルーティングライブラリAtaraxy、サンプルコード付きでプロジェクトが生成されます。

```sh
$ tree hello-duct
hello-duct
├── README.md
├── dev
│   ├── resources
│   │   └── dev.edn
│   └── src
│       ├── dev.clj
│       └── user.clj
├── project.clj
├── resources
│   └── hello_duct
│       ├── config.edn
│       └── public
├── src
│   ├── duct_hierarchy.edn
│   └── hello_duct
│       ├── handler
│       │   └── example.clj
│       └── main.clj
└── test
    └── hello_duct
        └── handler
            └── example_test.clj

12 directories, 10 files
```

ここからの具体的なWeb API開発の流れについては[ClojureのDuctでWeb API開発してみた](https://qiita.com/lagenorhynque/items/57d5aa086c4a080a1c54)などの例も参考にしてみてください。

ちなみに、Duct最新安定版はテンプレート名が `duct` ですが、alpha版は `duct-alpha` 、beta版は `duct-beta` のように命名されているので、例えば `lein new duct-beta ...` で執筆時点ではbeta版0.11.0-beta4のテンプレートでプロジェクトが生成されます。

Leiningenのテンプレート機能について詳しくは公式ドキュメント[leiningen/TEMPLATES.md](https://github.com/technomancy/leiningen/blob/master/doc/TEMPLATES.md)やソースコード[leiningen/new.clj](https://github.com/technomancy/leiningen/blob/master/src/leiningen/new.clj)が参考になるでしょう。

# [duct/lein-duct](https://github.com/duct-framework/duct/tree/master/lein-duct)

duct/lein-templateと並んでductリポジトリ配下にある"duct/lein-duct"はLeiningenのプラグインです。

Leiningenのプラグイン機能はコマンドラインから任意のタスクを実行可能にするものですが、執筆時点の最新安定版0.12.0の[src/leiningen/duct.clj](https://github.com/duct-framework/duct/blob/0.12.0/lein-duct/src/leiningen/duct.clj)を見てみると以下のような関数 [`duct`](https://github.com/duct-framework/duct/blob/0.12.0/lein-duct/src/leiningen/duct.clj#L19-L25) が定義されています。

```clj
(defn duct
  "Tasks for managing a Duct project."
  {:subtasks [#'setup]}
  [project subtask & args]
  (case subtask
    "setup"   (apply setup project args)
    (main/abort "Unknown duct subtask:" subtask)))
```

現状ではサブタスク `setup` のみが提供されていることが分かります[^3]。

先ほど `lein new duct` で生成した新規プロジェクト[hello-duct](https://github.com/lagenorhynque/hello-duct)にはすでにプラグインとしてlein-ductが設定されているので、早速 `lein duct setup` を実行してみましょう。

```sh
$ cd hello-duct/
$ cat project.clj | grep lein-duct
  :plugins [[duct/lein-duct "0.12.0"]]
  :middleware     [lein-duct.plugin/middleware]
$ lein duct setup
Created profiles.clj
Created .dir-locals.el
Created dev/resources/local.edn
Created dev/src/local.clj
```

ここで生成される4つのファイルはいずれもデフォルトで.gitignoreによりGitの管理対象から外れているため、個々の開発者がローカル環境で独自の設定を適用したい場合などに活用できます。

- `profiles.clj`: Leiningenプロジェクト設定[project.clj](https://github.com/lagenorhynque/hello-duct/blob/master/project.clj)のプロファイルを上書きする
- `.dir-locals.el`: Emacsのプロジェクト固有の設定をする(e.g. CIDERの挙動のカスタマイズ)
- `dev/resources/local.edn`: 開発環境のIntegrantコンポーネントの設定[dev/resources/dev.edn](https://github.com/lagenorhynque/hello-duct/blob/master/dev/resources/dev.edn)を上書きする
- `dev/src/local.clj`: REPLでの開発時の名前空間 `dev`([dev/src/dev.clj](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/dev.clj))の定義を上書きする

これらの設定ファイルはDuctでの開発で必須のものではありませんが、それぞれの役割を把握しておくと有用なこともあるでしょう(Ductプロジェクト以外でも設定手法として参考になります)。

また、duct/lein-ductには[src/lein_duct/plugin.clj](https://github.com/duct-framework/duct/blob/0.12.0/lein-duct/src/lein_duct/plugin.clj)に以下のような関数 `middleware` があります。

```clj
(defn middleware [project]
  (assoc-in project [:uberjar-merge-with "duct_hierarchy.edn"] `hierarchy-merger))
```

これはLeingenプラグインの["project middleware"](https://github.com/technomancy/leiningen/blob/master/doc/PLUGINS.md#project-middleware)と呼ばれるもので、ここでは `project.clj` のプロジェクトマップに `:uberjar-merge-with` の設定を追加する機能を果たしています。

Leinigenのプラグイン機能、プロファイル機能について詳しくは公式ドキュメント[leiningen/PLUGINS.md](https://github.com/technomancy/leiningen/blob/master/doc/PLUGINS.md), [leiningen/PROFILES.md](https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md)が参考になります。

# [duct/core](https://github.com/duct-framework/core)

Ductフレームワークのまさにコアとなっているのが"duct/core"です。

Webアプリのサーバサイド開発の基盤として使いやすく拡張しやすいように、Integrantをベースに便利な機能が提供されています。

先ほど用意したDuctプロジェクト[hello-duct](https://github.com/lagenorhynque/hello-duct)の内容を手がかりに、duct/coreがどのような役割を果たしているのか探ってみましょう。

## 開発環境でREPLから

[README.md](https://github.com/lagenorhynque/hello-duct/blob/master/README.md)の説明を参考に、REPLからシステムを操作してみます。

### 開発用名前空間への移動: `dev`

```clj
$ lein repl
nREPL server started on port 55259 on host 127.0.0.1 - nrepl://127.0.0.1:55259
REPL-y 0.4.3, nREPL 0.5.3
Clojure 1.10.0
Java HotSpot(TM) 64-Bit Server VM 11+28
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> (dev)
:loaded
dev=>
```

`lein repl` でREPLを起動して入ったデフォルトの名前空間 `user`([dev/src/user.clj](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/user.clj))で定義されている関数 [`dev`](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/user.clj#L3-L8) を呼び出すと、開発用の名前空間 `dev`([dev/src/dev.clj](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/dev.clj))がロードされて現在の名前空間に切り替わります[^4]。

### システムの起動: `go` (= `prep` + `init`)

名前空間 `dev` から関数 [`go`](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/dev.clj#L11) でシステムを起動することができます。

```clj
dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated
```

`http://localhost:3000/example` にアクセスしてみると、確かにAPIサーバが動作していることが確認できます。

```sh
$ curl http://localhost:3000/example
{"example":"data"}
```

この関数は [`integrant.repl/go`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L53-L55) で、IntegrantのREPL向けユーティリティライブラリ[integrant/repl](https://github.com/weavejester/integrant-repl)由来の関数のひとつです。

ソースを確認してみると、

```clj
dev=> (source go)
(defn go []
  (prep)
  (init))
nil
```

1. [`integrant.repl/prep`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L14-L17): Integrantの設定マップを準備(prepare)する
1. [`integrant.repl/init`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L47-L51) (内部的には [`integrant.core/init`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L410-L418)): 設定マップに基づいてシステムを初期化/起動(initialize)する

関数であることが分かります。

ここまでを見てみると、一見して特にduct/coreが登場することなくintegrant/replを介してIntegrantのシステム起動プロセスが呼び出されているだけのように見えます。

しかし実際にはDuct特有の処理が介在しています。

改めて現在の名前空間 `dev` に対応するソースコード[dev/src/dev.clj](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/dev.clj)を見てみましょう。

```clj:dev/src/dev.clj
(ns dev
  (:refer-clojure :exclude [test])
  (:require [clojure.repl :refer :all]
            [fipp.edn :refer [pprint]]
            [clojure.tools.namespace.repl :refer [refresh]]
            [clojure.java.io :as io]
            [duct.core :as duct]
            [duct.core.repl :as duct-repl]
            [eftest.runner :as eftest]
            [integrant.core :as ig]
            [integrant.repl :refer [clear halt go init prep reset]]
            [integrant.repl.state :refer [config system]]))

(duct/load-hierarchy)

(defn read-config []
  (duct/read-config (io/resource "hello_duct/config.edn")))

(defn test []
  (eftest/run-tests (eftest/find-tests "test")))

(def profiles
  [:duct.profile/dev :duct.profile/local])

(clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")

(when (io/resource "local.clj")
  (load "local"))

(integrant.repl/set-prep! #(duct/prep-config (read-config) profiles))
```

注目すべきはduct.coreの関数を利用しているトップレベルフォーム `(duct/load-hierarchy)` と `(integrant.repl/set-prep! #(duct/prep-config (read-config) profiles))` です。

#### コンポーネントのキー(キーワード)の階層関係の自動構築: `duct.core/load-hierarchy`

関数 [`duct.core/load-hierarchy`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L46-L66) は、クラスパス内の `duct_hierarchy.edn` というファイルを読み込み、マップデータをもとに `derive` でキーワード間の階層(親子)関係を構築します。

```clj
(defn load-hierarchy
  "Search the base classpath for files named `duct_hierarchy.edn`, and use them
  to extend the global `derive` hierarchy. This allows a hierarchy to be
  constructed without needing to load every namespace.

  The `duct_hierarchy.edn` file should be an edn map that maps child keywords
  to vectors of parents. For example:

      {:example/child [:example/father :example/mother]}

  This is equivalent to writing:

      (derive :example/child :example/father)
      (derive :example/child :example/mother)

  This function should be called once when the application is started."
  []
  (doseq [url (hierarchy-urls)]
    (let [hierarchy (edn/read-string (slurp url))]
      (doseq [[tag parents] hierarchy, parent parents]
        (derive tag parent)))))
```

Integrantではコンポーネントを識別するキーとしてキーワードを多用しますが、キーワードに階層関係を定義することによって派生した下位のキーワードを派生元の上位のキーワードで一般化する仕組みが活用されています。

そしてDuctでは、ednの設定ファイルでキーワードの関係を宣言的に記述して一括で自動ロードできるようになっています[^5]。

サンプルプロジェクトの名前空間 `dev` ではトップレベルで呼び出されているため、ロードされるたびに実行されます。

次に `(integrant.repl/set-prep! #(duct/prep-config (read-config) profiles))` を見てみます。

#### 独自拡張されたリーダによる設定ファイルの読み込み: `duct.core/read-config`

[`read-config`](https://github.com/lagenorhynque/hello-duct/blob/master/dev/src/dev.clj#L16-L17) は、設定ファイル[resources/hello_duct/config.edn](https://github.com/lagenorhynque/hello-duct/blob/master/resources/hello_duct/config.edn)を引数として `duct.core/read-config` を呼び出します。

```clj:dev/src/dev.clj#L16-L17
(defn read-config []
  (duct/read-config (io/resource "hello_duct/config.edn")))
```

関数 [`duct.core/read-config`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L116-L137) は、受け取ったファイルを [`integrant.core/read-string`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L148-L155) で読み取った結果を返します。

`integrant.core/read-string` はednファイルのタグ `#ig/ref` を 関数 [`integrant.core/ref`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L24-L28) の適用に、タグ `#ig/refset` を関数 [`integrant.core/refset`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L30-L34) の適用に読み替える機能を持っていますが、 `duct.core/read-config` ではそれに加えて

- `#duct/env`: 環境変数の値への解決
- `#duct/include`: 設定ファイルの組み込み
- `#duct/resource`: 関数 `clojure.java.io/resource` の適用

という追加のタグがサポートされています。

```clj
(defn read-config
  "Read an edn configuration from a slurpable source. An optional map of data
  readers may be supplied. By default the following five readers are supported:

  #duct/env
  : an environment variable, see [[duct.core.env/env]]

  #duct/include
  : substitute for a configuration on the classpath

  #duct/resource
  : a resource path string, see [[resource]]

  #ig/ref
  : an Integrant reference to another key

  #ig/refset
  : an Integrant reference to a set of keys"
  ([source]
   (read-config source {}))
  ([source readers]
   (some->> source slurp (ig/read-string {:readers (merge-default-readers readers)}))))
```

この関数は省略可能な第2引数としてデータリーダのマップを受け取ることができるため、必要に応じてフレームワーク利用者側で既存の実装を参考に独自のタグをサポートするようにさらに拡張するのも容易です。

#### 設定マップに対する独自の下処理: `duct.core/prep-config`

`#(duct/prep-config (read-config) profiles)` で上述の `read-config` の結果を入力としている関数が [`duct.core/prep-config`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L184-L195) です。

この関数は、設定ファイルから読み込まれてednのタグが処理された設定マップに対するさらなる下処理を進めます。

```clj
(defn prep-config
  "Load, build and prep a configuration of modules into an Integrant
  configuration that's ready to be initiated. This function loads in relevant
  namespaces based on key names, so is side-effectful (though idempotent)."
  ([config]
   (prep-config config :all))
  ([config profiles]
   (-> config
       (doto ig/load-namespaces)
       (build-config profiles)
       (doto ig/load-namespaces)
       (ig/prep))))
```

具体的には

1. [`integrant.core/load-namespaces`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L175-L188) で キーワードに対応する名前空間をロードする

1. [`duct.core/build-config`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L173-L182) でモジュール(`:duct/module` から派生したコンポーネント)とプロファイル(`:duct/profile` から派生したコンポーネント)から設定マップを組み立てる[^6]
    1. [`integrant.core/prep`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L399-L408) でモジュールの事前準備を行う
    1. [`integrant.core/init`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L410-L418) で引数 `profiles` で指定された(指定なしの場合はすべての)プロファイルの設定マップを [`duct.core/merge-configs`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L81-L86) によって賢くマージする[^7]
    1. [`duct.core/fold-modules`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L139-L145) でモジュールによる設定マップの変換を適用する

1. `integrant.core/load-namespaces` でキーワードに対応する名前空間をさらにロードする[^8]

1. [`integrant.core/prep`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L399-L408) でコンポーネントの事前準備を行う

サンプルプロジェクトの開発用名前空間 `dev` では、以上の `read-config` と `duct.core/prep-config` を組み合わせた `#(duct/prep-config (read-config) profiles)` が [`integrant.repl/set-prep!`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L8-L9) によって `integrant.repl/prep` 時に呼び出される関数として設定されています[^9]。

REPLから `go` 関数を実行すると、Integrantの機能に上乗せする形でこれだけの処理が走ってシステムが起動していたのです。

実はこれがDuctフレームワークが提供しているコア機能のほぼすべてです。

### システムの停止: `halt`

起動しているシステムを停止するには関数 `halt` を利用します。

```clj
dev=> (halt)
:duct.server.http.jetty/stopping-server
:halted
```

システムが停止した状態でAPIエンドポイントにアクセスしてみると、

```sh
$ curl http://localhost:3000/example
curl: (7) Failed to connect to localhost port 3000: Connection refused
```

確かにAPIサーバが停止していることが分かります。

この関数は [`integrant.repl/halt`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L62-L65)(内部的には [`integrant.core/halt!`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L420-L426)) で、システムのコンポーネントを停止します。

Duct特有の機能はありません。

### システムの再起動: `reset` (= `suspend` + `refresh` + `resume`)

また、システムを再起動するには関数 `reset` を利用します[^10]。

```clj
dev=> (reset)
:reloading (hello-duct.main dev hello-duct.handler.example hello-duct.handler.example-test user)
:duct.server.http.jetty/starting-server {:port 3000}
:resumed
```

関数 [`integrant.repl/reset`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L82-L84) は、ソースを確認すると次のように実装されていて、

```clj
dev=> (source reset)
(defn reset []
  (suspend)
  (repl/refresh :after 'integrant.repl/resume))
nil
```

1. [`integrant.repl/suspend`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L67-L69)(内部的には [`integrant.core/suspend!`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L452-L458)): システムを一時停止する
1. `clojure.tools.namespace.repl/refresh`: ソースコードの変更を検出してリロードする
1. [`integrant.repl/resume`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L71-L80)(内部的には [`integrant.repl/prep`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L14-L17) + [`integrant.core/resume`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L437-L450) または [`integrant.core/init`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L410-L418)): 設定マップを構築してシステムを再開/起動する

という流れで動作します。

関数 `integrant.repl/go` と同様に、 `integrant.repl/prep` が設定マップ構築のためにDuct独自の処理を実行しますが、それ以外はIntegrantの機能そのものです。

### `config` と `system`

`integrant.repl/prep` で構築された設定マップはREPLで `config` という変数で確認することができます。

```clj
dev=> (pprint config)
{:duct.handler.static/method-not-allowed {:body {:error :method-not-allowed}},
 :duct.logger/timbre {:level :debug,
                      :appenders {:duct.logger.timbre/spit #ig/ref :duct.logger.timbre/spit,
                                  :duct.logger.timbre/brief #ig/ref :duct.logger.timbre/brief}},
 :duct.logger.timbre/brief {:min-level :report},
 :duct.middleware.web/format {},
 :duct.router/ataraxy {:routes {[:get "/example"] [:hello-duct.handler/example]},
                       :handlers {:ataraxy.error/unmatched-path #ig/ref :duct.handler.static/not-found,
                                  :ataraxy.error/unmatched-method #ig/ref :duct.handler.static/method-not-allowed,
                                  :ataraxy.error/missing-params #ig/ref :duct.handler.static/bad-request,
                                  :ataraxy.error/missing-destruct #ig/ref :duct.handler.static/bad-request,
                                  :ataraxy.error/failed-coercions #ig/ref :duct.handler.static/bad-request,
                                  :ataraxy.error/failed-spec #ig/ref :duct.handler.static/bad-request,
                                  :hello-duct.handler/example #ig/ref :hello-duct.handler/example}},
 :duct.middleware.web/log-requests {:logger #ig/ref :duct/logger},
 :duct.middleware.web/defaults {:params {:urlencoded true,
                                         :keywordize true},
                                :responses {:not-modified-responses true,
                                            :absolute-redirects true,
                                            :content-types true,
                                            :default-charset "utf-8"}},
 :duct.server.http/jetty {:port 3000,
                          :handler #ig/ref :duct.handler/root,
                          :logger #ig/ref :duct/logger},
 :duct.handler.static/internal-server-error {:headers {"Content-Type" "application/json"},
                                             :body #duct/resource "duct/module/web/errors/500.json"},
 :duct.middleware.web/hide-errors {:error-handler #ig/ref :duct.handler.static/internal-server-error},
 :duct.logger.timbre/spit {:fname "logs/dev.log"},
 :duct.middleware.web/log-errors {:logger #ig/ref :duct/logger},
 :duct.handler/root {:router #ig/ref :duct/router,
                     :middleware [#ig/ref :duct.middleware.web/not-found
                                  #ig/ref :duct.middleware.web/format
                                  #ig/ref :duct.middleware.web/defaults
                                  #ig/ref :duct.middleware.web/log-requests
                                  #ig/ref :duct.middleware.web/log-errors
                                  #ig/ref :duct.middleware.web/stacktrace]},
 :duct.handler.static/not-found {:body {:error :not-found}},
 :duct.middleware.web/not-found {:error-handler #ig/ref :duct.handler.static/not-found},
 :duct.core/environment :development,
 :duct.middleware.web/stacktrace {},
 :duct.handler.static/bad-request {:body {:error :bad-request}},
 :duct.core/project-ns hello-duct,
 :hello-duct.handler/example {}}
nil
```

また、起動したシステムのマップはREPLで `system` という変数で確認できます。

```clj
dev=> (pprint system)
{:duct.handler.static/method-not-allowed #object[duct.handler.static$make_handler$fn__5997
                                                 "0x5e604f76"
                                                 "duct.handler.static$make_handler$fn__5997@5e604f76"],
 :duct.logger/timbre #duct.logger.timbre.TimbreLogger{:config {:level :debug,
                                                               :appenders {:duct.logger.timbre/spit {:enabled? true,
                                                                                                     :async? false,
                                                                                                     :min-level nil,
                                                                                                     :rate-limit nil,
                                                                                                     :output-fn :inherit,
                                                                                                     :fn #object[taoensso.timbre.appenders.core$spit_appender$self__13513
                                                                                                                 "0x706607cb"
                                                                                                                 "taoensso.timbre.appenders.core$spit_appender$self__13513@706607cb"]},
                                                                           :duct.logger.timbre/brief {:enabled? true,
                                                                                                      :async? false,
                                                                                                      :min-level :report,
                                                                                                      :rate-limit nil,
                                                                                                      :output-fn #object[duct.logger.timbre$brief_output_fn
                                                                                                                         "0x36ce3d31"
                                                                                                                         "duct.logger.timbre$brief_output_fn@36ce3d31"],
                                                                                                      :fn #object[taoensso.timbre.appenders.core$println_appender$fn__13503
                                                                                                                  "0x6a1ea958"
                                                                                                                  "taoensso.timbre.appenders.core$println_appender$fn__13503@6a1ea958"]}}}},
 :duct.logger.timbre/brief {:enabled? true,
                            :async? false,
                            :min-level :report,
                            :rate-limit nil,
                            :output-fn #object[duct.logger.timbre$brief_output_fn
                                               "0x36ce3d31"
                                               "duct.logger.timbre$brief_output_fn@36ce3d31"],
                            :fn #object[taoensso.timbre.appenders.core$println_appender$fn__13503
                                        "0x6a1ea958"
                                        "taoensso.timbre.appenders.core$println_appender$fn__13503@6a1ea958"]},
 :duct.middleware.web/format #object[duct.middleware.web$eval10207$fn__10208$fn__10209
                                     "0x1e0fa86f"
                                     "duct.middleware.web$eval10207$fn__10208$fn__10209@1e0fa86f"],
 :duct.router/ataraxy #object[ataraxy.core$handler$fn__10874
                              "0x3a930081"
                              "ataraxy.core$handler$fn__10874@3a930081"],
 :duct.middleware.web/log-requests #object[duct.middleware.web$eval10139$fn__10141$fn__10143
                                           "0x1a897506"
                                           "duct.middleware.web$eval10139$fn__10141$fn__10143@1a897506"],
 :duct.middleware.web/defaults #object[duct.middleware.web$eval10182$fn__10183$fn__10184
                                       "0x464e82de"
                                       "duct.middleware.web$eval10182$fn__10183$fn__10184@464e82de"],
 :duct.server.http/jetty {:handler #object[clojure.lang.Atom
                                           "0x7d48d8d"
                                           {:status :ready,
                                            :val #object[clojure.core$promise$reify__8486
                                                         "0x35e4dad4"
                                                         {:status :ready,
                                                          :val #object[ring.middleware.stacktrace$wrap_stacktrace_web$fn__10063
                                                                       "0x6aac0375"
                                                                       "ring.middleware.stacktrace$wrap_stacktrace_web$fn__10063@6aac0375"]}]}],
                          :logger #object[clojure.lang.Atom
                                          "0x5727d431"
                                          {:status :ready,
                                           :val #duct.logger.timbre.TimbreLogger{:config {:level :debug,
                                                                                          :appenders {:duct.logger.timbre/spit {:enabled? true,
                                                                                                                                :async? false,
                                                                                                                                :min-level nil,
                                                                                                                                :rate-limit nil,
                                                                                                                                :output-fn :inherit,
                                                                                                                                :fn #object[taoensso.timbre.appenders.core$spit_appender$self__13513
                                                                                                                                            "0x706607cb"
                                                                                                                                            "taoensso.timbre.appenders.core$spit_appender$self__13513@706607cb"]},
                                                                                                      :duct.logger.timbre/brief {:enabled? true,
                                                                                                                                 :async? false,
                                                                                                                                 :min-level :report,
                                                                                                                                 :rate-limit nil,
                                                                                                                                 :output-fn #object[duct.logger.timbre$brief_output_fn
                                                                                                                                                    "0x36ce3d31"
                                                                                                                                                    "duct.logger.timbre$brief_output_fn@36ce3d31"],
                                                                                                                                 :fn #object[taoensso.timbre.appenders.core$println_appender$fn__13503
                                                                                                                                             "0x6a1ea958"
                                                                                                                                             "taoensso.timbre.appenders.core$println_appender$fn__13503@6a1ea958"]}}}}}],
                          :server #object[org.eclipse.jetty.server.Server
                                          "0x75fcc3ff"
                                          "org.eclipse.jetty.server.Server@75fcc3ff"]},
 :duct.handler.static/internal-server-error #object[duct.handler.static$make_handler$fn__5997
                                                    "0x5624e934"
                                                    "duct.handler.static$make_handler$fn__5997@5624e934"],
 :duct.middleware.web/hide-errors #object[duct.middleware.web$eval10157$fn__10159$fn__10161
                                          "0x644ed95c"
                                          "duct.middleware.web$eval10157$fn__10159$fn__10161@644ed95c"],
 :duct.logger.timbre/spit {:enabled? true,
                           :async? false,
                           :min-level nil,
                           :rate-limit nil,
                           :output-fn :inherit,
                           :fn #object[taoensso.timbre.appenders.core$spit_appender$self__13513
                                       "0x706607cb"
                                       "taoensso.timbre.appenders.core$spit_appender$self__13513@706607cb"]},
 :duct.middleware.web/log-errors #object[duct.middleware.web$eval10148$fn__10150$fn__10152
                                         "0x49b7472b"
                                         "duct.middleware.web$eval10148$fn__10150$fn__10152@49b7472b"],
 :duct.handler/root #object[ring.middleware.stacktrace$wrap_stacktrace_web$fn__10063
                            "0x6aac0375"
                            "ring.middleware.stacktrace$wrap_stacktrace_web$fn__10063@6aac0375"],
 :duct.handler.static/not-found #object[duct.handler.static$make_handler$fn__5997
                                        "0x1b302a5d"
                                        "duct.handler.static$make_handler$fn__5997@1b302a5d"],
 :duct.middleware.web/not-found #object[duct.middleware.web$eval10166$fn__10168$fn__10170
                                        "0x926845e"
                                        "duct.middleware.web$eval10166$fn__10168$fn__10170@926845e"],
 :duct.core/environment :development,
 :duct.middleware.web/stacktrace #object[duct.middleware.web$eval10198$fn__10199$fn__10200
                                         "0x7adb144e"
                                         "duct.middleware.web$eval10198$fn__10199$fn__10200@7adb144e"],
 :duct.handler.static/bad-request #object[duct.handler.static$make_handler$fn__5997
                                          "0x2a2ff159"
                                          "duct.handler.static$make_handler$fn__5997@2a2ff159"],
 :duct.core/project-ns hello-duct,
 :hello-duct.handler/example #object[hello_duct.handler.example$eval14220$fn__14221$fn__14223
                                     "0x43c02e71"
                                     "hello_duct.handler.example$eval14220$fn__14221$fn__14223@43c02e71"]}
nil
```

## lein run/uberjarで `-main` 関数から

ここまでは開発環境でREPLからシステムを操作した場合の挙動を見てきましたが、 `lein run` や `lein uberjar` で生成したjarで `-main` 関数からシステムを起動する場合にはどのように動作するでしょうか。

サンプルプロジェクト"hello-duct"の[src/hello_duct/main.clj](https://github.com/lagenorhynque/hello-duct/blob/master/src/hello_duct/main.clj)は以下のようになっています。

```clj:src/hello_duct/main.clj
(ns hello-duct.main
  (:gen-class)
  (:require [duct.core :as duct]))

(duct/load-hierarchy)

(defn -main [& args]
  (let [keys     (or (duct/parse-keys args) [:duct/daemon])
        profiles [:duct.profile/prod]]
    (-> (duct/resource "hello_duct/config.edn")
        (duct/read-config)
        (duct/exec-config profiles keys))))
```

`dev/src/dev.clj` と比べた大きな違いは、

- `duct.core/parse-keys` でコマンドライン引数を処理し、キーとして `duct.core/exec-config` の第3引数に与えている
- `profiles` が異なる(開発環境とローカル環境独自の `[:duct.profile/dev :duct.profile/local]` ではなくプロダクション環境のみの `[:duct.profile/prod]`)
- `integrant.core/init` ではなく `duct.core/exec-config` でシステムを起動している

の3点です。

### コマンドライン引数のキーワード化: [`duct.core/parse-keys`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L197-L200)

キーワード形式の文字列シーケンスをキーワードシーケンスに変換するシンプルな関数です。

```clj
(defn parse-keys
  "Parse config keys from a sequence of command line arguments."
  [args]
  (seq (filter keyword? (map edn/read-string args))))
```

例えばこのように動作します。

```clj
dev=> (duct/parse-keys [":foo" ":bar-baz"])
(:foo :bar-baz)
```

したがって、

```clj
  (let [keys     (or (duct/parse-keys args) [:duct/daemon])
        profiles [:duct.profile/prod]]
    (-> ,,,
        (duct/exec-config profiles keys)))
```

というコードは、コマンドライン引数が与えられていればそのキーワード(シーケンス)、なければ `[:duct/daemon]` を第3引数として `duct.core/exec-config` を呼び出すことになります。

### スタンドアローン実行用に機能追加された `integrant.core/init`: [`duct.core/exec-config`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L213-L225)

`duct.core/prep-config` で設定マップを準備し、 `integrant.core/init` でシステムを起動することに加えて、 [`await-daemons`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core.clj#L205-L211) でキー `:duct/daemon` から派生したコンポーネントがある場合にシャットダウンフックでシステムが正しく停止するようにする機能を持つ、`-main` 関数での利用を想定した関数です。

```clj
(defn exec-config
  "Build, prep and initiate a configuration of modules, then block the thread
  (see [[await-daemons]]). By default it only runs profiles derived from
  `:duct.profile/prod` and keys derived from `:duct/daemon`.

  This function is designed to be called from `-main` when standalone operation
  is required."
  ([config]
   (exec-config config [:duct.profile/prod]))
  ([config profiles]
   (exec-config config profiles [:duct/daemon]))
  ([config profiles keys]
   (-> config (prep-config profiles) (ig/init keys) (await-daemons))))
```

以上のことから、 `-main` 関数を実行するとデフォルトではシステム全体のうち `:duct/daemon` キーワードから派生したコンポーネント(とそれが依存するコンポーネント)が起動し、シャットダウン時に停止するように振る舞うことが分かります。

# Ductモジュール

最後に、Ductフレームワークの機能拡張/再利用メカニズムである「モジュール」について探ってみましょう。

## Ductの「モジュール」とは

Ductにおけるモジュールについてはduct/coreの[README](https://github.com/duct-framework/core#modules)にもセクションがあります。

Ductモジュールとは、キーワード `:duct/module` から派生したキーのIntegrantコンポーネントであり、

- `integrant.core/prep-key` の実装(省略可能): モジュール適用の前提条件として要求するコンポーネントのキーを `:duct.core/requires` としてオプションマップに追加する(モジュールの他コンポーネントへの依存関係を定義することで適用順序が調整される)
- `integrant.core/init-key` の実装: 設定マップを受け取って設定マップを返す関数を返す(`duct.core/prep-config` 実行時にコンポーネントや設定の追加など、設定マップを変換する任意の処理が実行できる)

を持つもののことです。

例えばduct/coreのREADMEに示されている例では、仮に設定ファイルに `:duct.module/example {:port 8080}` という設定があれば `:duct.server/http` キーが存在する設定マップに `:duct.server/http {:port 8080}` という設定を追加するように動作するモジュール `:duct.module/example` を定義しています。

```clj
(require '[duct.core.merge :as merge])

(defmethod ig/prep-key :duct.module/example [_ opts]
  (assoc opts :duct.core/requires (ig/ref :duct.server/http)))

(defmethod ig/init-key :duct.module/example [_ {:keys [port]}]
  (fn [config]
    (duct/merge-configs
     config
     {:duct.server/http {:port (merge/displace port)}})))
```

コンポーネントのキーを `:duct/module` から派生させるには、すでに触れた関数 `duct.core/load-hierarchy` が自動的にロードして `derive` に展開してくれる設定ファイル `duct_hierarchy.edn` に定義しておくと便利です。

上の例では、以下の内容の `duct_hierarchy.edn` をクラスパス上に配置します。

```clj
{:duct.module/example [:duct/module]}
```

## 標準モジュール

モジュールは `duct.core/prep-config` 時に設定マップに対して任意の処理を実行するという非常に単純な仕組みなので様々な応用が考えられそうですが、最も一般的なユースケースは再利用可能なコンポーネントを選択的に追加することでフレームワークとしての機能を拡張するというものです。

実際にDuctフレームワークでは以下のようなモジュールが独立したライブラリとして提供されています。

- [duct/module.web](https://github.com/duct-framework/module.web)
- [duct/module.ataraxy](https://github.com/duct-framework/module.ataraxy)
- [duct/module.sql](https://github.com/duct-framework/module.sql)
- [duct/module.cljs](https://github.com/duct-framework/module.cljs)
- [duct/module.logging](https://github.com/duct-framework/module.logging)

project.cljに依存ライブラリとして追加し、設定ファイルにモジュールやモジュールを介して追加されるコンポーネントに対する設定を書き足すだけで、Webアプリのサーバ機能やDBアクセス機能、ClojureScriptフロントエンド開発機能などを使い始めることができます[^11][^12]。

## サードパーティモジュール

公式による標準提供のもの以外にもサードパーティによるDuctモジュールライブラリが開発され、公開されています。

cf. [Modules · duct-framework/duct Wiki](https://github.com/duct-framework/duct/wiki/Modules)

私自身も、最近までにDuctと[Pedestal](https://github.com/pedestal/pedestal)を組み合わせたWeb APIを開発する機会が仕事と趣味で何度かあり、標準モジュールduct/module.webの代わりにPedestalによるAPI開発機能の初期設定をコンポーネントとして組み込めるようにするモジュール[duct.module.pedestal](https://github.com/lagenorhynque/duct.module.pedestal)、標準モジュールduct/module.loggingの代わりに[Cambium](https://cambium-clojure.github.io/)によるJSON形式でのログ出力を可能にするモジュール[duct.module.cambium](https://github.com/lagenorhynque/duct.module.cambium)を書いて公開しました。

興味のある方は具体的な利用例として以下のサンプルプロジェクトも参考にしてみてください。

- [lagenorhynque/aqoursql](https://github.com/lagenorhynque/aqoursql): Duct + Pedestal + [Lacinia](https://github.com/walmartlabs/lacinia)のGraphQL APIサンプル実装
- [lagenorhynque/clj-rest-api](https://github.com/lagenorhynque/clj-rest-api): lagenorhynque/aqoursqlと同一のDBに対するDuct + PedestalのREST APIサンプル実装
- [lagenorhynque/route-guide](https://github.com/lagenorhynque/route-guide): Duct + Pedestal + [Protojure](https://github.com/protojure/lib)のgRPC APIサンプル実装

# Further Reading

- [Duct](https://github.com/duct-framework/duct)
    - [duct-framework/duct Wiki](https://github.com/duct-framework/duct/wiki): 貴重な公式ドキュメント
        - [Boundaries](https://github.com/duct-framework/duct/wiki/Boundaries): Duct固有の機能ではないため本記事では触れなかった、"boundary"プロトコルについて(DBなど外部サービスとの境界をプロトコルで疎結合にする設計パターンはDuctに限らず非常に有用)
    - [duct/UPGRADING.md](https://github.com/duct-framework/duct/blob/master/UPGRADING.md): 最新版に移行する場合のアップグレードガイド
- [duct/core](https://github.com/duct-framework/core): READMEに少ないながら重要な情報がまとまっている
- [Integrant](https://github.com/weavejester/integrant)
- [Clojureで快適なREPL駆動開発のために"reloaded workflow"を実践しよう](https://qiita.com/lagenorhynque/items/a752ffae490e83ebe70d)
- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)
- [Clojure/ClojureScript関連リンク集 > Webサーバサイド (Clojure)](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#web%E3%82%B5%E3%83%BC%E3%83%90%E3%82%B5%E3%82%A4%E3%83%89-clojure)

[^1]: したがってIntegrantの基本的な機能と扱い方を知っていると理解がスムーズです。ちなみにIntegrantや同種のライブラリ([Component](https://github.com/stuartsierra/component), [mount](https://github.com/tolitius/mount), etc.)が登場した背景については[Clojure Workflow Reloaded](https://cognitect.com/blog/2013/06/04/clojure-workflow-reloaded)が参考になります。
[^2]: Ductテンプレートに限りませんが `lein new duct :show` のようにしてテンプレートのドキュメントを確認することもできます。
[^3]: Ductプラグインに限りませんが `lein help duct` のようにすることでプラグインのヘルプも確認できます。
[^4]: このような仕組みを整えるのはREPLの起動失敗を防止するための工夫として一般的ですね。
[^5]: この機能は特に後述するDuctモジュールで活かされています。
[^6]: Ductのモジュールという特殊なコンポーネントの挙動については後述します。
[^7]: マージ戦略については名前空間 [`duct.core.merge`](https://github.com/duct-framework/core/blob/0.7.0/src/duct/core/merge.clj) 参照。メタデータを適切に設定することで設定ファイル間の優先度を調整できます。
[^8]: 設定マップのキー(キーワード)の名前に基づいて名前空間が自動的にロードされることから、コンポーネントのキーを適切に命名し適切な名前空間に配置すると、コンポーネントを定義した名前空間を明示的にロードする必要がなくなります。
[^9]: `duct.core/prep-config` の第2引数にプロファイルとして `[:duct.profile/dev :duct.profile/local]` を指定しているので、開発環境とローカル環境独自の設定マップが組み込まれることになります。
[^10]: `reset` は `halt` と `go` に相当する機能を内包していて、システムをまだ一度も起動していない状態でも動作するように実装されているため、REPL駆動開発のワークフローでは起動も再起動も `reset` だけで足ります。
[^11]: もちろん、効果的に活用するには個々のDuctモジュールがどのようなコンポーネントや設定を追加してくれるのか、どのようにカスタマイズできるのかを把握する必要があります。
[^12]: 標準モジュールについては、duct/lein-templateでプロジェクト生成時にプロファイルを指定すると対応するモジュールの初期設定がproject.cljと設定ファイルに自動追加されます。