ミニマリストのためのClojure REST API開発入門2 〜リファクタリング編〜

関数型言語とヨーロッパ言語が好きなClojurianの[lagénorhynque](https://twitter.com/lagenorhynque) (a.k.a. カマイルカ🐬)です(最近、[トルコ語](https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%AB%E3%82%B3%E8%AA%9E)を学ぶのが楽しい)。

2019年は、Lisperサークル[paren-holic](https://paren-holic.booth.pm/)を結成して[技術書典6](https://techbookfest.org/event/tbf06/circle/44710001)と[技術書典7](https://techbookfest.org/event/tbf07/circle/5691458736619520)に出展したり、Clojurian仲間とPodcast [(dosync radio)](https://anchor.fm/dosync-radio)を始めたり、会社主催で始めた関数型言語勉強会[Fun Fun Functional](https://opt.connpass.com/)での発表[*"Simple Made Easy" Made Easy*](https://www.slideshare.net/KentOhashi/simple-made-easy-made-easy)が[全文書き起こし記事](https://logmi.jp/tech/articles/321962)になったりと、個人的にもこれまでとはまた違った形でClojure/Lispコミュニティに関わることができたと感じる一年でした。

一方、仕事では昨年から開発を始めたREST APIに加えて今年の秋からGraphQL APIも[Clojure利用](https://github.com/athos/japanese-clojure-companies#%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BE%E3%82%AA%E3%83%97%E3%83%88-opt-technologies)を決め、[Clojurian Conquest](https://www.slideshare.net/KentOhashi/clojurian-conquest)(?)が着々と進んでいます。

以前に[ミニマリストのためのClojure REST API開発入門](https://qiita.com/lagenorhynque/items/b15689e5432e0170b172)という記事で(マイクロ)フレームワークを利用することなく最小構成でREST APIを開発する例をご紹介しました。

そこでは敢えて1ファイル(1名前空間)にすべてのコードを収めていましたが、本記事では、より快適に本格的に開発しやすく、今後の拡張をスムーズにするためにリファクタリングを試みましょう。

構成を整理するにあたり、全体的に[Duct](https://github.com/duct-framework/duct)フレームワークのスタイルを参考にしています。

サンプルコード:
- Leiningen版: [minimal-api-lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/tree/master/minimal-api-lein)
- Clojure CLI版: [minimal-api-clj.core](https://github.com/lagenorhynque/clojure-rest-api-quickstart/tree/master/minimal-api-clj.core)

※ 全体の[diff](https://github.com/lagenorhynque/clojure-rest-api-quickstart/compare/64ba26b2c36dfbff1078546f027e553c8a8f9724...c9a24b2374211aab57e5ce69393e429d9acd77fa) (初回執筆時点)

# 1. 依存ライブラリの最新化

※ コミット: [lein & clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/b6687ee46f88751a2e61e8c90d69c803e7fcf636) (初回執筆時点)

まずは、初期実装から時間も経過しているので、依存ライブラリの最新版を取り込んでみましょう。

Clojureは言語本体と標準ライブラリの後方互換性が非常に重視されていて、コミュニティ全体としても特によく使われる主要なライブラリは破壊的変更なく拡張されていくことが多いため、たいていの場合は素直に最新版に更新することができるでしょう(もちろん念のためCHANGELOGなどを確認しましょう)。

また、多機能の大きなライブラリ(フレームワーク)よりも単機能に特化した小さなライブラリを組み合わせることを好む傾向が強く、小さく安定したライブラリは良い意味で枯れているために数年以上の更新がないこともあります。

## Leiningenの場合

[lein-ancient](https://github.com/xsc/lein-ancient)というLeiningenプラグインを利用すると、更新可能な依存ライブラリを確認したり自動更新したりすることが簡単にできます。

ちなみに執筆時点のLeiningen最新版[2.9.5](https://github.com/technomancy/leiningen/blob/master/NEWS.md#295--2020-12-07)では、JDK 15を利用するといくつか警告が出力されるようです😅

```sh
$ cd minimal-api-lein/
$ lein ancient :all
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by clojure.lang.InjectedInvoker/0x0000000800b45840 to method com.sun.xml.internal.stream.XMLInputFactoryImpl.createXMLStreamReader(java.io.Reader)
WARNING: Please consider reporting this to the maintainers of clojure.lang.InjectedInvoker/0x0000000800b45840
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
[camel-snake-kebab "0.4.2"] is available but we use "0.4.0"
[org.clojure/clojure "1.10.1"] is available but we use "1.10.0" (use :check-clojure to upgrade)
[ring/ring-core "1.8.2"] is available but we use "1.7.1"
[ring/ring-jetty-adapter "1.8.2"] is available but we use "1.7.1"
[ring/ring-json "0.5.0"] is available but we use "0.4.0"
```

すべての依存ライブラリを最新化すると、以下のような出力に変わります。

```sh
$ lein ancient :all
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by clojure.lang.InjectedInvoker/0x0000000800b45840 to method com.sun.xml.internal.stream.XMLInputFactoryImpl.createXMLStreamReader(java.io.Reader)
WARNING: Please consider reporting this to the maintainers of clojure.lang.InjectedInvoker/0x0000000800b45840
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
all artifacts are up-to-date.
```

## Clojure CLIの場合

[Depot](https://github.com/Olical/depot)というライブラリを利用することで依存ライブラリの最新版の確認や更新ができます。

ここでは、あらかじめ[Usage](https://github.com/Olical/depot#usage)の手順通りに `~/.clojure/deps.edn` に `outdated` というエイリアスを設定してあります。

```sh
$ cd minimal-api-clj.core/
$ clojure -M:outdated --every
Checking for old versions in: deps.edn
  camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.0"} -> {:mvn/version "0.4.2"}
  org.clojure/clojure {:mvn/version "1.10.0"} -> {:mvn/version "1.10.1"}
  ring/ring-core {:mvn/version "1.7.1"} -> {:mvn/version "1.8.2"}
  ring/ring-jetty-adapter {:mvn/version "1.7.1"} -> {:mvn/version "1.8.2"}
  ring/ring-json {:mvn/version "0.4.0"} -> {:mvn/version "0.5.0"}
```

すべての依存ライブラリを最新化すると、以下のような出力に変わります。

```sh
$ clojure -M:outdated --every
Checking for old versions in: deps.edn
  All up to date!
```

# 2. Integrant-REPLの導入

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/ba9f2de087521c162c04df67de3af9d76b11cb68), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/f98b0407126d75d79aea8ca5a2f933855773b3ac) (初回執筆時点)

Lisp族の末裔(?)たるClojureでは、REPLを開発時に最大限に活用した「REPL駆動開発」(REPL-driven development)が当然の前提というべき開発スタイルですが、開発しているアプリケーション内で状態を持つもののライフサイクルを管理し、安全に継続的にリロード可能(reloadable)なREPL環境を実現するために[Integrant](https://github.com/weavejester/integrant), [Component](https://github.com/stuartsierra/component), [mount](https://github.com/tolitius/mount)などのライブラリを利用します(cf. [My Clojure Workflow, Reloaded](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded))。

前回の記事ですでにIntegrantを導入しているので、ここでは[Integrant-REPL](https://github.com/weavejester/integrant-repl)というライブラリを追加してREPLからより扱いやすい形に整えましょう。

## 依存ライブラリと設定の追加

Leiningenでは `project.clj` に依存ライブラリとしてIntegrant-REPLを追加するとともに、開発時のみ利用するソースとリソースのためのディレクトリ `dev/src`, `dev/resources` をパスに追加してみました。

また、REPL起動時の名前空間を `user` に変更しています。

```diff:minimal-api-lein/project.clj
@@ -13,4 +13,8 @@
                  [ring/ring-json "0.5.0"]]
   :main ^:skip-aot minimal-api-lein.core
   :target-path "target/%s"
-  :profiles {:uberjar {:aot :all}})
+  :profiles {:uberjar {:aot :all}
+             :dev {:source-paths ["dev/src"]
+                   :resource-paths ["dev/resources"]
+                   :dependencies [[integrant/repl "0.3.2"]]}
+             :repl {:repl-options {:init-ns user}}})
```

Clojure CLIでは書き方に少し差がありますが、ほぼ同等の設定が可能です。

```diff:minimal-api-clj.core/deps.edn
@@ -8,7 +8,9 @@
         ring/ring-jetty-adapter {:mvn/version "1.8.2"}
         ring/ring-json {:mvn/version "0.5.0"}}
  :aliases
- {:test {:extra-paths ["test"]
+ {:dev {:extra-paths ["dev/resources" "dev/src"]
+        :extra-deps {integrant/repl {:mvn/version "0.3.2"}}}
+  :test {:extra-paths ["test"]
          :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"}}}
   :runner
   {:extra-deps {com.cognitect/test-runner
```

## 開発用名前空間の定義

REPL起動時の名前空間を `user` としたので、その名前空間から開発時に便利な関数や変数にアクセスできるようにします。

`user` 名前空間に直接ユーティリティ関数などを定義していくと、対応するソース `user.clj` のロード時にエラーが発生した場合にREPL自体の起動に失敗してしまうことがあります。

それを避けるため、REPL起動後に入る `user` 名前空間から別の開発用名前空間(ここでは `dev`)に切り替える仕組みを用意します。

以下のように `user`, `dev` の各名前空間を定義します(前者は[Ductテンプレートの `user.clj`](https://github.com/duct-framework/duct/blob/0.12.1/lein-template/resources/leiningen/new/duct/base/user.clj) そのまま😅)。

```clj:minimal-api-lein/dev/src/user.clj
(ns user)

(defn dev
  "Load and switch to the 'dev' namespace."
  []
  (require 'dev)
  (in-ns 'dev)
  :loaded)
```

```clj:minimal-api-lein/dev/src/dev.clj
(ns dev
  (:require
   [clojure.java.io :as io]
   [clojure.repl :refer :all]
   [clojure.spec.alpha :as s]
   [clojure.tools.namespace.repl :refer [refresh]]
   [integrant.core :as ig]
   [integrant.repl :refer [clear halt go init prep reset]]
   [integrant.repl.state :refer [config system]]
   [minimal-api-lein.core]))

(integrant.repl/set-prep! (constantly minimal-api-lein.core/config))
```

このようにすると、 `user` 名前空間から `dev` 関数を呼び出すと `dev` 名前空間のロードと切り替えが行われ、 `dev` 名前空間を起点に様々な操作をすることができます。

`dev` 名前空間には必要に応じてrequireするlibを追加したりユーティリティ関数を定義したりすると良いでしょう。

ここでは、最終行で [`integrant.repl/set-prep!`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L8-L9) 関数によりこのアプリケーションの設定マップ `minimal-api-lein.core/config` をIntegrant-REPLの仕組みに組み込んでいます(cf. [Usage](https://github.com/weavejester/integrant-repl#usage))。

## 動作確認

Leiningenでは `lein repl` 、Clojure CLIでは `clj -M:dev` でREPLを起動してIntegrant-REPLによるAPIの起動/停止を確かめてみましょう。

```clj
$ lein repl
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
nREPL server started on port 54498 on host 127.0.0.1 - nrepl://127.0.0.1:54498
REPL-y 0.4.4, nREPL 0.8.3
Clojure 1.10.1
OpenJDK 64-Bit Server VM 15+36-1562
    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)` で `user` から `dev` へと名前空間を切り替え、

```clj
user=> (dev)
2019-12-22 17:47:46.099:INFO::nRepl-session-68275480-c28b-4b45-9aeb-6f17bed10ddf: Logging initialized @22084ms to org.eclipse.jetty.util.log.StdErrLog
:loaded
dev=>
```

`(reset)` とすると、ログ出力からも分かるようにAPIサーバが起動(または再起動)します。

ちなみにIntegrant-REPLの [`integrant.repl`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj) 名前空間では `reset` のほかに `clear`, `halt`, `go`, `init`, `prep` などREPLからシステムの状態を変える関数が提供されていますが、普段のワークフローではシステムの起動/再起動(+リロード)の `reset` と停止の `halt` という2つだけ覚えておけば実用上十分です。

```clj
dev=> (reset)
:reloading (minimal-api-lein.core minimal-api-lein.core-test dev user)
2019-12-22 17:48:25.597:INFO:oejs.Server:nRepl-session-68275480-c28b-4b45-9aeb-6f17bed10ddf: jetty-9.4.22.v20191022; built: 2019-10-22T13:37:13.455Z; git: b1e6b55512e008f7fbdf1cbea4ff8a6446d1073b; jvm 13.0.1+9
2019-12-22 17:48:25.624:INFO:oejs.AbstractConnector:nRepl-session-68275480-c28b-4b45-9aeb-6f17bed10ddf: Started ServerConnector@5920aca0{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
2019-12-22 17:48:25.625:INFO:oejs.Server:nRepl-session-68275480-c28b-4b45-9aeb-6f17bed10ddf: Started @61609ms
:resumed
```

[`integrant.repl.state/config`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl/state.clj#L6) には設定マップが、 [`integrant.repl.state/system`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl/state.clj#L7) にはシステムマップが自動的に束縛されます。

```clj
dev=> (clojure.pprint/pprint config)
#:minimal-api-lein.core{:routes {},
                        :app
                        {:routes {:key :minimal-api-lein.core/routes}},
                        :server
                        {:app {:key :minimal-api-lein.core/app},
                         :options {:port 3000, :join? false}}}
nil
dev=> (clojure.pprint/pprint system)
#:minimal-api-lein.core{:routes
                        ["/"
                         {"todos"
                          {:get
                           #object[minimal_api_lein.core$list_todos 0x1137ebd3 "minimal_api_lein.core$list_todos@1137ebd3"],
                           :post
                           #object[minimal_api_lein.core$create_todo 0x2f6e91cd "minimal_api_lein.core$create_todo@2f6e91cd"]},
                          ["todos/" :todo-id]
                          {:get
                           #object[minimal_api_lein.core$fetch_todo 0x49f1736a "minimal_api_lein.core$fetch_todo@49f1736a"],
                           :delete
                           #object[minimal_api_lein.core$delete_todo 0x7b40a452 "minimal_api_lein.core$delete_todo@7b40a452"],
                           :put
                           #object[minimal_api_lein.core$update_todo 0x65440c92 "minimal_api_lein.core$update_todo@65440c92"]}}],
                        :app
                        #object[ring.middleware.params$wrap_params$fn__5534 0x4c860f13 "ring.middleware.params$wrap_params$fn__5534@4c860f13"],
                        :server
                        #object[org.eclipse.jetty.server.Server 0x7959ea8b "Server@7959ea8b{STARTED}[9.4.22.v20191022]"]}
nil
```

`curl` でアクセスしてみると、期待通りREST APIが動作していることが確認できます。

```sh
$ curl -s "http://localhost:3000/todos" -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /todos HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 22 Dec 2019 08:48:59 GMT
< Content-Type: application/json;charset=utf-8
< Transfer-Encoding: chunked
< Server: Jetty(9.4.22.v20191022)
<
{ [89 bytes data]
* Connection #0 to host localhost left intact
{
  "todo1": {
    "task": "build an API"
  },
  "todo2": {
    "task": "?????"
  },
  "todo3": {
    "task": "profit!"
  }
}
```

そして `(halt)` でシステムを停止すると、

```clj
dev=> (halt)
2019-12-22 17:49:09.178:INFO:oejs.AbstractConnector:nRepl-session-68275480-c28b-4b45-9aeb-6f17bed10ddf: Stopped ServerConnector@5920aca0{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
:halted
```

`system` の値が `nil` に変わり、

```clj
dev=> (clojure.pprint/pprint config)
#:minimal-api-lein.core{:routes {},
                        :app
                        {:routes {:key :minimal-api-lein.core/routes}},
                        :server
                        {:app {:key :minimal-api-lein.core/app},
                         :options {:port 3000, :join? false}}}
nil
dev=> (clojure.pprint/pprint system)
nil
nil
```

`curl` の応答からAPIサーバが停止していることが確認できます。

```sh
$ curl -s "http://localhost:3000/todos" -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 3000 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connection failed
* connect to 127.0.0.1 port 3000 failed: Connection refused
* Failed to connect to localhost port 3000: Connection refused
* Closing connection 0
```

## (Emacs/Spacemacsユーザ向け) 便利な設定の追加

EmacsまたはSpacemacsでClojure開発環境として[CIDER](https://github.com/clojure-emacs/cider)を利用している場合、[Miscellaneous Features :: CIDER Docs > Reloading Code](https://docs.cider.mx/cider/usage/misc_features.html#reloading-code)にあるように `cider-ns-refresh`(= [clojure.tools.namespace](https://github.com/clojure/tools.namespace)の `refresh` 系関数呼び出し)の前後に任意の関数をフックすることができます。

Integrant-REPLの [`reset`](https://github.com/weavejester/integrant-repl/blob/0.3.1/src/integrant/repl.clj#L82-L84)関数がしているのは `integrant.repl/suspend`, `clojure.tools.namespace.repl/refresh`, `integrant.repl/resume` を順に呼び出すことなので、以下のようにEmacsのローカル設定ファイル `.dir-locals.el` を用意すると `cider-ns-refresh` (SpacemacsのEvilキーバインドなら `, e n r`)でREPL上での `(reset)` 呼び出しを代用することが可能になります。

```emacs-lisp:minimal-api-lein/.dir-locals.el
((nil . ((cider-ns-refresh-before-fn . "integrant.repl/suspend")
         (cider-ns-refresh-after-fn  . "integrant.repl/resume"))))
```

# 3. テストの整備

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/b849d5495d9c4e5a00e077db5ca33b06ac242560), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/6ac93c2c3915d4b22f5f3d306041697f6a6e9663) (初回執筆時点)

テストコードのないコードベースに対して「リファクタリング」と称して危険な賭けを敢行するのは避けたいものです😅

本格的に既存コードに手を入れる前に、既存の振る舞いを保証するためのテストを整備しましょう。

ここでは、REST APIに対するE2E的なテストを用意し、静的解析ツールとともにCIで自動実行できるようにします。

## 依存ライブラリと設定の追加

Clojureで定番のHTTPクライアント[clj-http](https://github.com/dakrone/clj-http)、テスト結果の出力を見やすくする[humane-test-output](https://github.com/pjstadig/humane-test-output)を開発時の依存ライブラリに追加し、またLeinigenプラグインとしてリンターの[Eastwood](https://github.com/jonase/eastwood)、テストカバレッジ計測の[cloverage](https://github.com/cloverage/cloverage)、イディオムチェッカーの[kibit](https://github.com/jonase/kibit)を追加しています。

また、コマンドラインから便利に使えるように `test-coverage` 、 `lint` というエイリアスも設定してみました。

```diff:minimal-api-lein/project.clj
@@ -16,5 +16,21 @@
   :profiles {:uberjar {:aot :all}
              :dev {:source-paths ["dev/src"]
                    :resource-paths ["dev/resources"]
-                   :dependencies [[integrant/repl "0.3.2"]]}
+                   :dependencies [[clj-http "3.11.0"]
+                                  [integrant/repl "0.3.2"]
+                                  [pjstadig/humane-test-output "0.10.0"]]
+                   :plugins [[jonase/eastwood "0.3.12"]
+                             [lein-cloverage "1.2.1"]
+                             [lein-kibit "0.1.8"]]
+                   :aliases {"test-coverage" ^{:doc "Execute cloverage."}
+                             ["cloverage" "--ns-exclude-regex" "^(:?dev|user)$" "--codecov" "--junit"]
+                             "lint" ^{:doc "Execute eastwood and kibit."}
+                             ["do"
+                              ["eastwood" "{:source-paths [\"src\"]
+                                            :test-paths []}"]
+                              ["kibit"]]}
+                   :injections [(require 'pjstadig.humane-test-output)
+                                (pjstadig.humane-test-output/activate!)]}
              :repl {:repl-options {:init-ns user}}})
```

Clojure CLIでは一部使えない(使うのが難しそう)なツールもありますが、機能的に近い設定を以下のように書いてみました。

```diff:minimal-api-clj.core/deps.edn
@@ -9,7 +9,8 @@
         ring/ring-json {:mvn/version "0.5.0"}}
  :aliases
  {:dev {:extra-paths ["dev/resources" "dev/src"]
-        :extra-deps {integrant/repl {:mvn/version "0.3.2"}}}
+        :extra-deps {clj-http {:mvn/version "3.11.0"}
+                     integrant/repl {:mvn/version "0.3.2"}}}
   :test {:extra-paths ["test"]
          :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"}}}
   :runner
@@ -17,4 +18,15 @@
                "-d" "test"]}
   :uberjar {:extra-deps {seancorfield/depstar {:mvn/version "2.0.161"}}
             :main-opts ["-m" "hf.depstar.uberjar" "minimal-api-clj.core.jar"
-                        "-C" "-m" "minimal-api-clj.core"]}}}
+                        "-C" "-m" "minimal-api-clj.core"]}
+  :cloverage
+  {:extra-deps {cloverage {:mvn/version "1.2.1"}}
+   :main-opts ["-m" "cloverage.coverage"
+               "--src-ns-path" "src" "--test-ns-path" "test" "--codecov" "--junit"]}
+  :eastwood
+  {:extra-deps {jonase/eastwood {:mvn/version "0.3.12"}}
+   :main-opts ["-m" "eastwood.lint" {:source-paths ["src"]
+                                     :test-paths []}]}}}
```

さらに、リンターの[clj-kondo](https://github.com/borkdude/clj-kondo)と[Joker](https://github.com/candid82/joker)、フォーマッターの[cljstyle](https://github.com/greglook/cljstyle)の設定ファイルを追加し、 `make` でコマンドラインから手軽に実行できるようにしてみました。

これらの静的解析ツールはエディタ/IDEで自動実行するように設定しておくと非常に便利です。

- [Clojure開発環境でのリンターclj-kondo, Joker設定まとめ](https://qiita.com/lagenorhynque/items/dd9d6a1d97cbea738bc0)
- [Clojure開発環境でのフォーマッターcljstyle設定まとめ](https://qiita.com/lagenorhynque/items/a5d83b4a36a1cf1cacbe)

```clj:minimal-api-lein/.clj-kondo/config.edn
{:lint-as {clojure.test.check.clojure-test/defspec clojure.test/deftest
           clojure.test.check.properties/for-all clojure.core/for
           minimal-api-lein.test-helper/with-system clojure.core/with-open}
 :linters {:unresolved-namespace {:exclude []}}}
```

```clj:minimal-api-lein/.joker
{:ignored-file-regexes [#"dev/.+"
                        #"profiles.clj"]
 :ignored-unused-namespaces []
 :known-macros [clojure.spec.alpha/fdef
                clojure.test.check.clojure-test/defspec
                clojure.test.check.properties/for-all
                minimal-api-lein.test-helper/with-system]
 :known-tags []}
```

```clj:minimal-api-lein/.cljstyle
{:files {:ignore #{}}
 :rules {:blank-lines {:max-consecutive 1
                       :padding-lines 1}
         :functions {:enabled? false}
         :indentation {:indents {defspec [[:inner 0]]
                                 fdef [[:inner 0]]
                                 for-all [[:inner 0]]}
                       :list-indent 1}
         :namespaces {:indent-size 1}
         :types {:enabled? false}}}
```

```make:minimal-api-lein/Makefile
.PHONY: cljstyle-check
cljstyle-check:
	@cljstyle check

.PHONY: cljstyle-fix
cljstyle-fix:
	@cljstyle fix

.PHONY: clj-kondo-lint
clj-kondo-lint:
	@clj-kondo --lint src test

.PHONY: joker-lint
joker-lint:
	@joker --lint --working-dir .

.PHONY: lint
lint:
	@make cljstyle-check clj-kondo-lint joker-lint
```

## テスト支援ユーティリティの実装

必要なライブラリと設定は追加したので早速テストコードを書いていきたいところですが、あらかじめ今回書こうとするテストコードでよくあるパターンを関数やマクロとして定義しておくと便利です。

Integrantでライフサイクル管理されたシステムをテスト用の設定で起動/停止できるようにするマクロ `with-system` 、テスト時の(インメモリ)DBの初期データを投入し元に戻すマクロ `with-db-data` 、HTTPアクセスを楽にする `http-*` 関数を用意してみました。

ここでは簡単さを優先してテスト用の設定マップを `minimal-api-lein.core/config` から加工する方法を採用しました(cf. `test-config` 関数)が、テスト時固有の設定マップを差分だけ別途定義しておき、[Meta-Merge](https://github.com/weavejester/meta-merge)のようなライブラリで賢くマージさせるような方法も考えられます。

```clj:minimal-api-lein/test/minimal_api_lein/test_helper.clj
(ns minimal-api-lein.test-helper
  (:require
   [clj-http.client :as client]
   [integrant.core :as ig]
   [minimal-api-lein.core :as core]))

(defn test-config []
  (assoc-in core/config [::core/server :options :port] 3001))

(defn test-system []
  (ig/prep (test-config)))

(def test-url-prefix "http://localhost:3001")

;;; macros for testing context

(defmacro with-system [[bound-sym binding-expr] & body]
  `(let [~bound-sym (ig/init ~binding-expr)]
     (try
       ~@body
       (finally (ig/halt! ~bound-sym)))))

(defmacro with-db-data [db-data-map & body]
  `(let [old-val# @core/todos]
     (try
       (reset! core/todos ~db-data-map)
       ~@body
       (finally (reset! core/todos old-val#)))))

;;; HTTP client

(defn http-get [path]
  (client/get (str test-url-prefix path)
              {:accept :json
               :as :json
               :coerce :always
               :throw-exceptions? false}))

(defn http-post [path body]
  (client/post (str test-url-prefix path)
               {:form-params body
                :content-type :json
                :accept :json
                :as :json
                :coerce :always
                :throw-exceptions? false}))

(defn http-put [path body]
  (client/put (str test-url-prefix path)
              {:form-params body
               :content-type :json
               :accept :json
               :as :json
               :coerce :always
               :throw-exceptions? false}))

(defn http-delete [path]
  (client/delete (str test-url-prefix path)
                 {:accept :json
                  :as :json
                  :coerce :always
                  :throw-exceptions? false}))
```

## テストケースの実装

テスト用のユーティリティが整備されていれば、例えば以下のように簡潔にREST APIに対するテストを記述することができます。

```clj:minimal-api-lein/test/minimal_api_lein/core_test.clj
(ns minimal-api-lein.core-test
  (:require
   [clojure.test :as t]
   [minimal-api-lein.test-helper :as helper :refer [with-db-data with-system]]))

(t/deftest test-api
  (with-system [sys (helper/test-system)]
    (with-db-data {"todo1" {"task" "build an API"}
                   "todo2" {"task" "?????"}
                   "todo3" {"task" "profit!"}}
      (t/testing "TODOリストの一覧が取得できる"
        (let [{:keys [status body]} (helper/http-get "/todos")]
          (t/is (= 200 status))
          (t/is (= {:todo1 {:task "build an API"}
                    :todo2 {:task "?????"}
                    :todo3 {:task "profit!"}}
                   body))))
      (t/testing "指定したIDのTODOが取得できる"
        (let [{:keys [status body]} (helper/http-get "/todos/todo3")]
          (t/is (= 200 status))
          (t/is (= {:task "profit!"}
                   body))))
      (t/testing "指定したIDのTODOが削除できる"
        (let [{:keys [status body]} (helper/http-delete "/todos/todo2")]
          (t/is (= 204 status))
          (t/is (nil? body))))
      (t/testing "TODOが追加できる"
        (let [{:keys [status body]} (helper/http-post "/todos" {:task "something new"})]
          (t/is (= 201 status))
          (t/is (= {:task "something new"}
                   body))))
      (t/testing "指定したIDのTODOが更新できる"
        (let [{:keys [status body]} (helper/http-put "/todos/todo3" {:task "something different"})]
          (t/is (= 201 status))
          (t/is (= {:task "something different"}
                   body))))
      (t/testing "更新内容を反映したTODOリストの一覧が取得できる"
        (let [{:keys [status body]} (helper/http-get "/todos")]
          (t/is (= 200 status))
          (t/is (= {:todo1 {:task "build an API"}
                    :todo3 {:task "something different"}
                    :todo4 {:task "something new"}}
                   body))))
      (t/testing "存在しないIDのTODOを取得しようとするとエラー"
        (let [{:keys [status body]} (helper/http-get "/todos/todo10")]
          (t/is (= 404 status))
          (t/is (= {:message "Todo todo10 doesn't exist"}
                   body))))
      (t/testing "存在しないIDのTODOを削除しようとするとエラー"
        (let [{:keys [status body]} (helper/http-delete "/todos/todo10")]
          (t/is (= 404 status))
          (t/is (= {:message "Todo todo10 doesn't exist"}
                   body))))
      (t/testing "存在しないIDのTODOを更新しようとすると作成される"
        (let [{:keys [status body]} (helper/http-put "/todos/todo10" {:task "another one"})]
          (t/is (= 201 status))
          (t/is (= {:task "another one"}
                   body))
          (let [{:keys [status body]} (helper/http-get "/todos")]
            (t/is (= 200 status))
            (t/is (= {:todo1 {:task "build an API"}
                      :todo3 {:task "something different"}
                      :todo4 {:task "something new"}
                      :todo10 {:task "another one"}}
                     body))))))))
```

## テストの実行

コマンドラインからは `lein test` (Clojure CLIでは `clojure -M:dev:test:runner`)で単純なテスト実行、 `lein test-coverage` (Clojure CLIでは `clojure -M:dev:test:cloverage`)でカバレッジ計測付きのテスト実行が可能です。

```sh
$ lein test
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
2019-12-25 20:14:33.681:INFO::main: Logging initialized @7290ms to org.eclipse.jetty.util.log.StdErrLog

lein test minimal-api-lein.core-test
2019-12-25 20:14:37.915:INFO:oejs.Server:main: jetty-9.4.22.v20191022; built: 2019-10-22T13:37:13.455Z; git: b1e6b55512e008f7fbdf1cbea4ff8a6446d1073b; jvm 13.0.1+9
2019-12-25 20:14:37.959:INFO:oejs.AbstractConnector:main: Started ServerConnector@350ede1{HTTP/1.1,[http/1.1]}{0.0.0.0:3001}
2019-12-25 20:14:37.959:INFO:oejs.Server:main: Started @11569ms
2019-12-25 20:14:38.201:INFO:oejs.AbstractConnector:main: Stopped ServerConnector@350ede1{HTTP/1.1,[http/1.1]}{0.0.0.0:3001}

lein test minimal-api-lein.test-helper

Ran 1 tests containing 20 assertions.
0 failures, 0 errors.
```

```sh
$ lein test-coverage
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
Retrieving org/clojure/tools.reader/1.1.2/tools.reader-1.1.2.pom from central
Retrieving org/clojure/tools.reader/1.1.2/tools.reader-1.1.2.jar from central
WARNING: reader-conditional already refers to: #'clojure.core/reader-conditional in namespace: clojure.tools.reader.impl.utils, being replaced by: #'clojure.tools.reader.impl.utils/reader-conditional
WARNING: tagged-literal already refers to: #'clojure.core/tagged-literal in namespace: clojure.tools.reader.impl.utils, being replaced by: #'clojure.tools.reader.impl.utils/tagged-literal
Loading namespaces:  (minimal-api-lein.core)
Test namespaces:  (minimal-api-lein.test-helper minimal-api-lein.core-test)
2019-12-25 20:17:17.866:INFO::main: Logging initialized @5909ms to org.eclipse.jetty.util.log.StdErrLog
Loaded  minimal-api-lein.core  .
Instrumented namespaces.
2019-12-25 20:17:20.806:INFO:oejs.Server:main: jetty-9.4.22.v20191022; built: 2019-10-22T13:37:13.455Z; git: b1e6b55512e008f7fbdf1cbea4ff8a6446d1073b; jvm 13.0.1+9
2019-12-25 20:17:20.876:INFO:oejs.AbstractConnector:main: Started ServerConnector@10bf2726{HTTP/1.1,[http/1.1]}{0.0.0.0:3001}
2019-12-25 20:17:20.876:INFO:oejs.Server:main: Started @8920ms
2019-12-25 20:17:21.217:INFO:oejs.AbstractConnector:main: Stopped ServerConnector@10bf2726{HTTP/1.1,[http/1.1]}{0.0.0.0:3001}
Ran tests.
Writing HTML report to: /Users/lagenorhynque/programming/clojure-rest-api-quickstart/minimal-api-lein/target/coverage/index.html
Writing codecov.io report to: /Users/lagenorhynque/programming/clojure-rest-api-quickstart/minimal-api-lein/target/coverage/codecov.json

|-----------------------+---------+---------|
|             Namespace | % Forms | % Lines |
|-----------------------+---------+---------|
| minimal-api-lein.core |   98.82 |   98.31 |
|-----------------------+---------+---------|
|             ALL FILES |   98.82 |   98.31 |
|-----------------------+---------+---------|
```

また、LeiningenやClojure CLIの起動のオーバーヘッドを考えると、開発時に起動中のREPLから素早くテストが実行できると非常に捗ります。

エディタ/IDEのプラグインが提供する機能でキーバインドで手軽にREPLからテストを実行できることが多いですが、開発用名前空間 `dev` に例えば以下のような `test` 関数を用意してみても良いかもしれません(さらに[Eftest](https://github.com/weavejester/eftest)のようなサードパーティのテストランナーを導入する案もあります)。

```diff:minimal-api-lein/dev/src/dev.clj
@@ -1,7 +1,9 @@
 (ns dev
+  (:refer-clojure :exclude [test])
   (:require
    [clojure.java.io :as io]
    [clojure.repl :refer :all]
    [clojure.spec.alpha :as s]
+   [clojure.test :as test]
    [clojure.tools.namespace.repl :refer [refresh]]
    [integrant.core :as ig]
    [integrant.repl :refer [clear halt go init prep reset]]
@@ -9,3 +11,7 @@
             [minimal-api-lein.core]))

 (integrant.repl/set-prep! (constantly minimal-api-lein.core/config))
+
+(defn test
+  ([] (test/run-all-tests #"minimal-api-lein\..+-test"))
+  ([ns-sym] (test/run-tests ns-sym)))
```

## CIの設定

最後にテストと静的解析ツールをCIで自動実行させるための設定を追加します。

例えば[CircleCI](https://circleci.com/gh/lagenorhynque/clojure-rest-api-quickstart)向けに以下のように設定することができるでしょう。

```yaml:.circleci/config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/clojure-rest-api-quickstart
    docker:
    - image: circleci/clojure:openjdk-11-lein-2.9.5
      environment:
        TZ: Asia/Tokyo
    steps:
    - checkout
    - run:
        name: Install dependencies
        command: |
          curl -O https://download.clojure.org/install/linux-install-1.10.1.763.sh
          chmod +x linux-install-1.10.1.763.sh
          sudo ./linux-install-1.10.1.763.sh
    - restore_cache:
        key: minimal-api-lein-{{ checksum "minimal-api-lein/project.clj" }}
    - restore_cache:
        key: minimal-api-clj-{{ checksum "minimal-api-clj.core/deps.edn" }}
    - run: cd minimal-api-lein; lein deps
    - run: cd minimal-api-lein; lein test-coverage
    - run: cd minimal-api-clj.core; clojure -M:dev:test:cloverage
    - run: cd minimal-api-lein; lein lint
    - run: cd minimal-api-clj.core; clojure -M:eastwood
    - run:
        name: Install static code analysis tools
        environment:
          CLJSTYLE_VERSION: 0.14.0
          CLJ_KONDO_VERSION: 2020.12.12
          JOKER_VERSION: 0.15.7
        command: |
          # cljstyle
          wget https://github.com/greglook/cljstyle/releases/download/${CLJSTYLE_VERSION}/cljstyle_${CLJSTYLE_VERSION}_linux.tar.gz
          tar -xzf cljstyle_${CLJSTYLE_VERSION}_linux.tar.gz
          sudo mv -f cljstyle /usr/local/bin/
          # clj-kondo
          curl -sLO https://raw.githubusercontent.com/borkdude/clj-kondo/master/script/install-clj-kondo
          chmod +x install-clj-kondo
          sudo ./install-clj-kondo --version ${CLJ_KONDO_VERSION}
          # Joker
          wget https://github.com/candid82/joker/releases/download/v${JOKER_VERSION}/joker-${JOKER_VERSION}-linux-amd64.zip
          unzip -qq joker-${JOKER_VERSION}-linux-amd64.zip
          sudo mv -f joker /usr/local/bin/
    - run: cd minimal-api-lein; make lint
    - run: cd minimal-api-clj.core; make lint
    - run: bash <(curl -s https://codecov.io/bash) -f '!*.txt'
    - save_cache:
        key: minimal-api-lein-{{ checksum "minimal-api-lein/project.clj" }}
        paths:
        - ~/.lein
        - ~/.m2
    - save_cache:
        key: minimal-api-clj-{{ checksum "minimal-api-clj.core/deps.edn" }}
        paths:
        - ~/.cpcache
        - ~/.m2
    - run:
        name: Save test results
        command: |
          mkdir -p ~/test-results/lein-test
          if [ -f minimal-api-lein/target/coverage/junit.xml ]; then
            cp minimal-api-lein/target/coverage/junit.xml ~/test-results/lein-test/
          fi
          mkdir -p ~/test-results/clj-test
          if [ -f minimal-api-clj.core/target/coverage/junit.xml ]; then
            cp minimal-api-clj.core/target/coverage/junit.xml ~/test-results/clj-test/
          fi
        when: always
    - store_test_results:
        path: ~/test-results
```

# 4. 名前空間の分割と設定の外部化

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/c50ed39a19ed00f9485d019b198212b6a0e044e6), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/097be54b6b564e6b61e1b26994a78b5f0165c950) (初回執筆時点)

テストケースとCI環境を用意して高いテストカバレッジも確認できたので、いよいよ本格的なリファクタリングに取り組んでみましょう。

前回の記事における初期実装では1名前空間にすべてを定義していましたが、アプリケーション内での責務に合う形で名前空間を分割し、Integrantの設定マップもこのタイミングで[edn](https://github.com/edn-format/edn)ファイルとして外部化します。

個々の独立した名前空間(モジュール)に分けることで単純にコードの見通しが良くなるのはもちろん、それぞれの名前空間を凝集性高く疎結合に構成していくことで変更の影響を局所化しながら拡張しやすいクリーンなアーキテクチャの実現にも繋がります。

## 名前空間の定義と紐付け

もとの名前空間 `minimal-api-lein.core` はアプリケーションのエントリーポイントとしての責務に特化させ、残る責務に対応する形で以下のように名前空間を分割してみます。

- `minimal-api-lein.db`: (インメモリ)DBの定義
- `minimal-api-lein.handler`: HTTPのリクエストを受け取ってレスポンスを返すハンドラ関数の定義
- `minimal-api-lein.middleware`: ハンドラ関数に共通の事前/事後処理を行うミドルウェアの定義
- `minimal-api-lein.routes`: APIのルーティングの定義
- `minimal-api-lein.server`: APIサーバの定義

```clj:minimal-api-lein/src/minimal_api_lein/db.clj
(ns minimal-api-lein.db)

(def todos
  (atom {"todo1" {"task" "build an API"}
         "todo2" {"task" "?????"}
         "todo3" {"task" "profit!"}}))
```

```clj:minimal-api-lein/src/minimal_api_lein/handler.clj
(ns minimal-api-lein.handler
  (:require
   [clojure.string :as str]
   [minimal-api-lein.db :refer [todos]]
   [ring.util.http-response :as response]))

(defn list-todos [_]
  (response/ok @todos))

(defn create-todo [{:keys [params]}]
  (let [id (->> (keys @todos)
                (map #(-> %
                          (str/replace-first "todo" "")
                          Long/parseLong))
                (apply max)
                inc)
        todo-id (str "todo" id)]
    (swap! todos assoc todo-id {"task" (:task params)})
    (response/created (str "/todos/" todo-id) (get @todos todo-id))))

(defn fetch-todo [{:keys [params]}]
  (if-let [todo (get @todos (:todo-id params))]
    (response/ok todo)
    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

(defn delete-todo [{:keys [params]}]
  (if (get @todos (:todo-id params))
    (do (swap! todos dissoc (:todo-id params))
        (response/no-content))
    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

(defn update-todo [{:keys [params]}]
  (let [task {"task" (:task params)}]
    (swap! todos assoc (:todo-id params) task)
    (response/created (str "/todos/" (:todo-id params)) task)))
```

```clj:minimal-api-lein/src/minimal_api_lein/middleware.clj
(ns minimal-api-lein.middleware
  (:require
   [camel-snake-kebab.core :refer [->kebab-case ->snake_case]]
   [camel-snake-kebab.extras :refer [transform-keys]]))

(defn wrap-kebab-case-keys [handler]
  (fn [request]
    (let [response (-> request
                       (update :params (partial transform-keys #(->kebab-case % :separator \_)))
                       handler)]
      (transform-keys #(->snake_case % :separator \-) response))))
```

```clj:minimal-api-lein/src/minimal_api_lein/routes.clj
(ns minimal-api-lein.routes
  (:require
   [integrant.core :as ig]
   [minimal-api-lein.handler :as handler]))

(defmethod ig/init-key ::routes [_ _]
  ["/" {"todos" {:get handler/list-todos
                 :post handler/create-todo}
        ["todos/" :todo-id] {:get handler/fetch-todo
                             :delete handler/delete-todo
                             :put handler/update-todo}}])
```

```clj:minimal-api-lein/src/minimal_api_lein/server.clj
(ns minimal-api-lein.server
  (:require
   [bidi.ring :refer [make-handler]]
   [integrant.core :as ig]
   [minimal-api-lein.middleware :refer [wrap-kebab-case-keys]]
   [ring.adapter.jetty :as jetty]
   [ring.middleware.json :refer [wrap-json-params wrap-json-response]]
   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
   [ring.middleware.params :refer [wrap-params]]))

(defmethod ig/init-key ::app [_ {:keys [routes]}]
  (-> (make-handler routes)
      wrap-kebab-case-keys
      wrap-keyword-params
      wrap-json-params
      wrap-json-response
      wrap-params))

(defmethod ig/init-key ::server [_ {:keys [app options]}]
  (jetty/run-jetty app options))

(defmethod ig/halt-key! ::server [_ server]
  (.stop server))
```

また、 `minimal-api-lein.core` 名前空間に定義されていた設定マップ `config` は `resources/config.edn` として外部ファイル化します。

名前空間分割により設定マップのキー名(= Integrantのコンポーネント名)が変わることに注意が必要です([Integrantの名前空間自動ロード機能](https://github.com/weavejester/integrant#loading-namespaces)を活かすには、コンポーネントを定義した名前空間に対応するキー名を採用するのがオススメです)。

ednファイルでは [`integrant.core/ref`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L24-L28) 関数の代わりに `#ig/ref` タグを利用します。

```clj:minimal-api-lein/resources/config.edn
{:minimal-api-lein.routes/routes {}
 :minimal-api-lein.server/app {:routes #ig/ref :minimal-api-lein.routes/routes}
 :minimal-api-lein.server/server {:app #ig/ref :minimal-api-lein.server/app
                                  :options {:port 3000
                                            :join? false}}}
```

## システム起動方法の変更

システムの設定マップをednファイルに抽出したことにより、Integrantを介したシステムの起動方法も自ずと変更することになります。

システムを起動するのは、本番アプリケーションのためのメインエントリーポイント、開発時のREPL、テスト時のテスト支援ユーティリティの3箇所なので、それぞれに修正が必要です。

### メインエントリーポイント

名前空間分割により `minimal-api-lein.core` 名前空間には `-main` 関数だけが残ることになります。

設定マップ `config` の代わりにリソースから `config.edn` の内容を [`integrant.core/read-string`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L148-L155) で読み込み、 [`integrant.core/load-namespaces`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L175-L188) で必要な名前空間を自動ロードさせてから [`integrant.core/prep`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L399-L408) 、 [`integrant.core/init`](https://github.com/weavejester/integrant/blob/0.7.0/src/integrant/core.cljc#L410-L418) すれば良いでしょう。

```diff:minimal-api-lein/src/minimal_api_lein/core.clj
@@ -1,97 +1,12 @@
 (ns minimal-api-lein.core
   (:gen-class)
   (:require
-   [bidi.ring :refer [make-handler]]
-   [camel-snake-kebab.core :refer [->kebab-case ->snake_case]]
-   [camel-snake-kebab.extras :refer [transform-keys]]
-   [clojure.string :as str]
-   [integrant.core :as ig]
-   [ring.adapter.jetty :as jetty]
-   [ring.middleware.json :refer [wrap-json-params wrap-json-response]]
-   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
-   [ring.middleware.params :refer [wrap-params]]
-   [ring.util.http-response :as response]))
-
-;;; handlers
-
-(def todos
-  (atom {"todo1" {"task" "build an API"}
-         "todo2" {"task" "?????"}
-         "todo3" {"task" "profit!"}}))
-
-(defn list-todos [_]
-  (response/ok @todos))
-
-(defn create-todo [{:keys [params]}]
-  (let [id (->> (keys @todos)
-                (map #(-> %
-                          (str/replace-first "todo" "")
-                          Long/parseLong))
-                (apply max)
-                inc)
-        todo-id (str "todo" id)]
-    (swap! todos assoc todo-id {"task" (:task params)})
-    (response/created (str "/todos/" todo-id) (get @todos todo-id))))
-
-(defn fetch-todo [{:keys [params]}]
-  (if-let [todo (get @todos (:todo-id params))]
-    (response/ok todo)
-    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))
-
-(defn delete-todo [{:keys [params]}]
-  (if (get @todos (:todo-id params))
-    (do (swap! todos dissoc (:todo-id params))
-        (response/no-content))
-    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))
-
-(defn update-todo [{:keys [params]}]
-  (let [task {"task" (:task params)}]
-    (swap! todos assoc (:todo-id params) task)
-    (response/created (str "/todos/" (:todo-id params)) task)))
-
-;;; routes
-
-(defmethod ig/init-key ::routes [_ _]
-  ["/" {"todos" {:get list-todos
-                 :post create-todo}
-        ["todos/" :todo-id] {:get fetch-todo
-                             :delete delete-todo
-                             :put update-todo}}])
-
-;;; middleware
-
-(defn wrap-kebab-case-keys [handler]
-  (fn [request]
-    (let [response (-> request
-                       (update :params (partial transform-keys #(->kebab-case % :separator \_)))
-                       handler)]
-      (transform-keys #(->snake_case % :separator \-) response))))
-
-(defmethod ig/init-key ::app [_ {:keys [routes]}]
-  (-> (make-handler routes)
-      wrap-kebab-case-keys
-      wrap-keyword-params
-      wrap-json-params
-      wrap-json-response
-      wrap-params))
-
-;;; API server
-
-(defmethod ig/init-key ::server [_ {:keys [app options]}]
-  (jetty/run-jetty app options))
-
-(defmethod ig/halt-key! ::server [_ server]
-  (.stop server))
-
-;;; system configuration
-
-(def config
-  {::routes {}
-   ::app {:routes (ig/ref ::routes)}
-   ::server {:app (ig/ref ::app)
-             :options {:port 3000
-                       :join? false}}})
-
-;;; main entry point
-
-(defn -main [& args]
-  (ig/init config))
+   [clojure.java.io :as io]
+   [integrant.core :as ig]))
+
+(defn -main [& _]
+  (-> (io/resource "config.edn")
+      slurp
+      ig/read-string
+      (doto ig/load-namespaces)
+      ig/prep
+      ig/init))
```

### 開発時

REPLから `reset` と `halt` で(再)起動/停止できるようにするには、設定マップ `config` の代わりに `config.edn` から読み取ったマップを(名前空間自動ロードさせてから) Integrant-REPLの仕組みに組み込めば良いので、例えば以下のように `dev` 名前空間に設定します。

```diff:minimal-api-lein/dev/src/dev.clj
@@ -7,10 +7,15 @@
             [clojure.tools.namespace.repl :refer [refresh]]
             [integrant.core :as ig]
             [integrant.repl :refer [clear halt go init prep reset]]
-            [integrant.repl.state :refer [config system]]
-            [minimal-api-lein.core]))
+            [integrant.repl.state :refer [config system]]))

-(integrant.repl/set-prep! (constantly minimal-api-lein.core/config))
+(defn read-config []
+  (-> (io/resource "config.edn")
+      slurp
+      ig/read-string
+      (doto ig/load-namespaces)))
+
+(integrant.repl/set-prep! (comp ig/prep read-config))

 (defn test
   ([] (test/run-all-tests #"minimal-api-lein\..+-test"))
```

### テスト時

開発時の `dev` 名前空間と同様に `config.edn` から読み込んだ内容をシステムの設定マップとして利用します。

```diff:minimal-api-lein/test/minimal_api_lein/test_helper.clj
@@ -1,10 +1,15 @@
 (ns minimal-api-lein.test-helper
   (:require
    [clj-http.client :as client]
+   [clojure.java.io :as io]
    [integrant.core :as ig]
-   [minimal-api-lein.core :as core]))
+   [minimal-api-lein.db :as db]))

 (defn test-config []
-  (assoc-in core/config [::core/server :options :port] 3001))
+  (-> (io/resource "config.edn")
+      slurp
+      ig/read-string
+      (assoc-in [:minimal-api-lein.server/server :options :port] 3001)
+      (doto ig/load-namespaces)))

 (defn test-system []
   (ig/prep (test-config)))
@@ -20,11 +25,11 @@
        (finally (ig/halt! ~bound-sym)))))

 (defmacro with-db-data [db-data-map & body]
-  `(let [old-val# @core/todos]
+  `(let [old-val# @db/todos]
      (try
-       (reset! core/todos ~db-data-map)
+       (reset! db/todos ~db-data-map)
        ~@body
-       (finally (reset! core/todos old-val#)))))
+       (finally (reset! db/todos old-val#)))))

 ;;; HTTP client

```

以上の対応により、これまでと変わらずREPLから `reset`/`halt` したり、テストを実行したり、 `lein run -m minimal-api-lein.core` (Clojure CLIでは `clojure -M -m minimal-api-clj.core`)でAPIをコマンドライン起動したりすることができます。

# 5. "boundary"プロトコルによる疎結合化

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/4eae1b85b798ba093e62eeaec3765756616d2076), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/3f8edcc9131ec9ed5a53376f56cceb29a2b773da) (初回執筆時点)

名前空間分割により生まれた `minimal-api-lein.handler` 名前空間と `minimal-api-lein.db` 名前空間に注目してみると、HTTPリクエスト/レスポンスを扱う各種ハンドラ関数が(インメモリ)DBの実装の詳細(今回は簡単に `atom` で可変状態を実現していること、内部に保持されているマップの構造など)を知りすぎている極めて密結合な状態であることに気づきます。

レイヤーの異なる名前空間(モジュール)同士の疎結合性を高める(言い換えれば[「依存関係逆転の原則」(dependency inversion principle; DIP)](https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87)に従う)ため、Clojureの[プロトコル(protocol)](https://clojure.org/reference/protocols)を活用してDBアクセスのインターフェースと実装を分離しましょう。

Ductフレームワークではこの種の外部サービス/リソースとの境界を定義するようなプロトコルを["boundary"](https://github.com/duct-framework/duct/wiki/Boundaries)(バウンダリ)と呼んでいるため、ここでもその命名を踏襲します。

## インメモリDBアクセス用のユーティリティの実装

外部リソースとしてのインメモリDBに対するアクセスに関する汎用的なユーティリティのために、 `minimal-api-lein.boundary.db.core` という名前空間を新たに用意しましょう。

この名前空間には、 `:data` というひとつのフィールドを持つレコード `Boundary` を定義し、新規のIntegrantコンポーネント `::db` (= `:minimal-api-lein.boundary.db.core/db`)がインメモリDBのデータを `:data` フィールドに持つ `Boundary` として初期化されるようにします(ちなみに `->Boundary` は `Boundary` という名前でレコードを定義すると自動生成されるコンストラクタ関数です)。

また、インメモリDBのマップ(`Boundary` の `:data` フィールド)はTODOデータ以外も管理できるようにTODOデータは `:todo` キーに持たせるように変更しています。

そしてインメモリDBに対する典型的なアクセスパターンをユーティリティ関数 `select-all`, `select-by-key`, `insert!`, `update-by-key!`, `delete-by-key!` として定義しておきます。

```clj:minimal-api-lein/src/minimal_api_lein/boundary/db/core.clj
(ns minimal-api-lein.boundary.db.core
  (:require
   [clojure.string :as str]
   [integrant.core :as ig]))

(defrecord Boundary [data])

(defmethod ig/init-key ::db [_ _]
  (->Boundary (atom {:todo {"todo1" {"task" "build an API"}
                            "todo2" {"task" "?????"}
                            "todo3" {"task" "profit!"}}})))

(defn select-all [{:keys [data]} table]
  (get @data table))

(defn selet-by-key [{:keys [data]} table k]
  (get-in @data [table k]))

(defn insert! [{:keys [data]} table key-prefix v]
  (let [id (->> (keys (get @data table))
                (map #(-> %
                          (str/replace-first key-prefix "")
                          Long/parseLong))
                (apply max)
                inc)
        k (str key-prefix id)]
    (swap! data assoc-in [table k] v)
    k))

(defn update-by-key! [{:keys [data]} table k v]
  (swap! data assoc-in [table k] v)
  nil)

(defn delete-by-key! [{:keys [data]} table k]
  (swap! data update table dissoc k)
  nil)
```

## TODOデータにアクセスするためのプロトコル(DBバウンダリ)の定義と実装

汎用的なインメモリDBアクセスユーティリティが用意できたら、TODOデータにアクセスするためのインターフェースをプロトコルとして定義し、 `minimal_api_lein.boundary.db.core.Boundary` の場合の実装を与えます。

これは、例えば関数 `minimal-api-lein.boundary.db.todo/find-todos` の第1引数に `minimal_api_lein.boundary.db.core.Boundary` 型の値を与えると、 `(db/select-all db :todo)` という実装が呼び出されることを意味します。

```clj:minimal-api-lein/src/minimal_api_lein/boundary/db/todo.clj
(ns minimal-api-lein.boundary.db.todo
  (:require
   [minimal-api-lein.boundary.db.core :as db]))

(defprotocol Todo
  (find-todos [db])
  (find-todo-by-id [db id])
  (create-todo! [db todo])
  (update-todo! [db id todo])
  (delete-todo! [db id]))

(extend-protocol Todo
  minimal_api_lein.boundary.db.core.Boundary
  (find-todos [db]
    (db/select-all db :todo))
  (find-todo-by-id [db id]
    (db/selet-by-key db :todo id))
  (create-todo! [db todo]
    (db/insert! db :todo "todo" todo))
  (update-todo! [db id todo]
    (db/update-by-key! db :todo id todo))
  (delete-todo! [db id]
    (db/delete-by-key! db :todo id)))
```

## DB情報のリクエストマップへの注入(DI)

インメモリDBからTODOデータにアクセスする関数は実装できたので、あとはハンドラ関数から呼び出せるようにする必要があります。

ハンドラ関数に `minimal_api_lein.boundary.db.core.Boundary`(= `:minimal-api-lein.boundary.db.core/db` コンポーネント) の値を渡すことができれば良いので、ここではミドルウェアを利用してハンドラ関数の入力であるリクエストマップに注入することにします。

まず、設定マップでは `:minimal-api-lein.server/app` コンポーネントに `:minimal-api-lein.boundary.db.core/db` コンポーネントを参照させます。

```diff:minimal-api-lein/resources/config.edn
@@ -1,5 +1,7 @@
-{:minimal-api-lein.routes/routes {}
- :minimal-api-lein.server/app {:routes #ig/ref :minimal-api-lein.routes/routes}
+{:minimal-api-lein.boundary.db.core/db {}
+ :minimal-api-lein.routes/routes {}
+ :minimal-api-lein.server/app {:routes #ig/ref :minimal-api-lein.routes/routes
+                               :db #ig/ref :minimal-api-lein.boundary.db.core/db}
  :minimal-api-lein.server/server {:app #ig/ref :minimal-api-lein.server/app
                                   :options {:port 3000
                                             :join? false}}}
```

ミドルウェア `wrap-db` は第2引数 `db` の値を `:db` というキーでリクエストマップに追加します。

```diff:minimal-api-lein/src/minimal_api_lein/middleware.clj
@@ -8,3 +8,7 @@
                        (update :params (partial transform-keys #(->kebab-case % :separator \_)))
                        handler)]
       (transform-keys #(->snake_case % :separator \-) response))))
+
+(defn wrap-db [handler db]
+  (fn [request]
+    (handler (assoc request :db db))))
```

`:minimal-api-lein.server/app` コンポーネントは `:minimal-api-lein.boundary.db.core/db` コンポーネントの値を受け取れるので、それをミドルウェア `minimal-api-lein.middleware/wrap-db` 経由でリクエストマップに差し込みます。

```diff:minimal-api-lein/src/minimal_api_lein/server.clj
@@ -1,19 +1,20 @@
 (ns minimal-api-lein.server
   (:require
    [bidi.ring :refer [make-handler]]
    [integrant.core :as ig]
-   [minimal-api-lein.middleware :refer [wrap-kebab-case-keys]]
+   [minimal-api-lein.middleware :refer [wrap-db wrap-kebab-case-keys]]
    [ring.adapter.jetty :as jetty]
    [ring.middleware.json :refer [wrap-json-params wrap-json-response]]
    [ring.middleware.keyword-params :refer [wrap-keyword-params]]
    [ring.middleware.params :refer [wrap-params]]))

-(defmethod ig/init-key ::app [_ {:keys [routes]}]
+(defmethod ig/init-key ::app [_ {:keys [routes db]}]
   (-> (make-handler routes)
       wrap-kebab-case-keys
       wrap-keyword-params
       wrap-json-params
       wrap-json-response
-      wrap-params))
+      wrap-params
+      (wrap-db db)))

 (defmethod ig/init-key ::server [_ {:keys [app options]}]
   (jetty/run-jetty app options))
```

## DBバウンダリ関数の動作確認

ここで `minimal-api-lein.boundary.db.todo` に定義した `Todo` プロトコル関数(DBバウンダリ関数)の動作を確認しておきましょう。

以下のようなユーティリティ関数を開発用名前空間 `dev` に用意しておくと便利かもしれません。

```diff:minimal-api-lein/dev/src/dev.clj
@@ -20,3 +20,9 @@
 (defn test
   ([] (test/run-all-tests #"minimal-api-lein\..+-test"))
   ([ns-sym] (test/run-tests ns-sym)))
+
+(defn db []
+  (:minimal-api-lein.boundary.db.core/db system))
+
+(defn db-run [f & args]
+  (apply f (db) args))
```

`(reset)` でシステムを起動してから `db-run` 関数を利用してDBバウンダリ関数を試してみると、期待通りに動作していることが確認できます。

```clj
dev=> (db-run minimal-api-lein.boundary.db.todo/find-todos)
{"todo1" {"task" "build an API"}, "todo2" {"task" "?????"}, "todo3" {"task" "profit!"}}
dev=> (db-run minimal-api-lein.boundary.db.todo/find-todo-by-id "todo2")
{"task" "?????"}
dev=> (db-run minimal-api-lein.boundary.db.todo/create-todo! {"task" "something new"})
"todo4"
dev=> (db-run minimal-api-lein.boundary.db.todo/update-todo! "todo2" {"task" "something different"})
nil
dev=> (db-run minimal-api-lein.boundary.db.todo/delete-todo! "todo3")
nil
dev=> (db-run minimal-api-lein.boundary.db.todo/find-todos)
{"todo1" {"task" "build an API"}, "todo2" {"task" "something different"}, "todo4" {"task" "something new"}}
```

## DBバウンダリ関数を利用したハンドラ関数の改修

ハンドラ関数がリクエストマップから `minimal_api_lein.boundary.db.core.Boundary` の値を取り出せるようになったので、あとは各ハンドラ関数から `minimal-api-lein.boundary.db.todo` にある `Todo` プロトコルの関数を利用するだけです。

これにより、 `minimal-api-lein.handler` 名前空間のハンドラ関数は `Todo` プロトコルというインターフェースに依存するのみでインメモリDBという実装の詳細に対する依存がなくなります。

ハンドラ関数は、TODOデータがインメモリDBにあるのか、RDBにあるのか、ファイルにあるのか、外部APIに由来するのかさえ知らない状態にすることができたわけです。

```diff:minimal-api-lein/src/minimal_api_lein/handler.clj
@@ -1,34 +1,27 @@
 (ns minimal-api-lein.handler
   (:require
-   [clojure.string :as str]
-   [minimal-api-lein.db :refer [todos]]
+   [minimal-api-lein.boundary.db.todo :as db.todo]
    [ring.util.http-response :as response]))

-(defn list-todos [_]
-  (response/ok @todos))
+(defn list-todos [{:keys [db]}]
+  (response/ok (db.todo/find-todos db)))

-(defn create-todo [{:keys [params]}]
-  (let [id (->> (keys @todos)
-                (map #(-> %
-                          (str/replace-first "todo" "")
-                          Long/parseLong))
-                (apply max)
-                inc)
-        todo-id (str "todo" id)]
-    (swap! todos assoc todo-id {"task" (:task params)})
-    (response/created (str "/todos/" todo-id) (get @todos todo-id))))
+(defn create-todo [{:keys [params db]}]
+  (let [id (db.todo/create-todo! db {"task" (:task params)})
+        todo (db.todo/find-todo-by-id db id)]
+    (response/created (str "/todos/" id) todo)))

-(defn fetch-todo [{:keys [params]}]
-  (if-let [todo (get @todos (:todo-id params))]
+(defn fetch-todo [{:keys [params db]}]
+  (if-let [todo (db.todo/find-todo-by-id db (:todo-id params))]
     (response/ok todo)
     (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

-(defn delete-todo [{:keys [params]}]
-  (if (get @todos (:todo-id params))
-    (do (swap! todos dissoc (:todo-id params))
+(defn delete-todo [{:keys [params db]}]
+  (if (db.todo/find-todo-by-id db (:todo-id params))
+    (do (db.todo/delete-todo! db (:todo-id params))
         (response/no-content))
     (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

-(defn update-todo [{:keys [params]}]
+(defn update-todo [{:keys [params db]}]
   (let [task {"task" (:task params)}]
-    (swap! todos assoc (:todo-id params) task)
+    (db.todo/update-todo! db (:todo-id params) task)
     (response/created (str "/todos/" (:todo-id params)) task)))
```

## テストコードの改修

最後に、インメモリDBの管理方法が変わったのでテスト時の初期データ投入のためのマクロ `wit-db-data` を改修します。

```diff:minimal-api-lein/test/minimal_api_lein/test_helper.clj
@@ -1,8 +1,7 @@
 (ns minimal-api-lein.test-helper
   (:require
    [clj-http.client :as client]
    [clojure.java.io :as io]
-   [integrant.core :as ig]
-   [minimal-api-lein.db :as db]))
+   [integrant.core :as ig]))

 (defn test-config []
   (-> (io/resource "config.edn")
@@ -24,12 +23,13 @@
        ~@body
        (finally (ig/halt! ~bound-sym)))))

-(defmacro with-db-data [db-data-map & body]
-  `(let [old-val# @db/todos]
+(defmacro with-db-data [[system db-data-map] & body]
+  `(let [db# (:minimal-api-lein.boundary.db.core/db ~system)
+         old-val# @(:data db#)]
      (try
-       (reset! db/todos ~db-data-map)
+       (reset! (:data db#) ~db-data-map)
        ~@body
-       (finally (reset! db/todos old-val#)))))
+       (finally (reset! (:data db#) old-val#)))))

 ;;; HTTP client

```

テストケース側でも `with-db-data` マクロとインメモリDBのデータ構造の変更を取り込めば、今まで通りすべてのテストがパスするはずです。

```diff:minimal-api-lein/test/minimal_api_lein/core_test.clj
@@ -5,9 +5,9 @@

 (t/deftest test-api
   (with-system [sys (helper/test-system)]
-    (with-db-data {"todo1" {"task" "build an API"}
-                   "todo2" {"task" "?????"}
-                   "todo3" {"task" "profit!"}}
+    (with-db-data [sys {:todo {"todo1" {"task" "build an API"}
+                               "todo2" {"task" "?????"}
+                               "todo3" {"task" "profit!"}}}]
       (t/testing "TODOリストの一覧が取得できる"
         (let [{:keys [status body]} (helper/http-get "/todos")]
           (t/is (= 200 status))
```

# 6. clojure.specの導入

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/414dcad3c03b2e095e4abedef7f290f89b88335f), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/c9a24b2374211aab57e5ce69393e429d9acd77fa) (初回執筆時点)

今回のAPIでは(インメモリ)DBに対するアクセスがビジネスロジックとして非常に重要ですが、DBアクセス関数の入出力データがどのようなものであるか必ずしも明らかでないという問題が残っています。

開発時やテスト時に想定しない入出力に対して分かりやすい形でfail-fastになるように、clojure.specでDBアクセス関数を保護することにしましょう。

## specの定義

まず `minimal-api-lein.boundary.db.core` 名前空間では `::db`(= `:minimal-api-lein.boundary.db.core/db`) とは `:data` フィールドを持つマップであるとspec定義します。

```diff:minimal-api-lein/src/minimal_api_lein/boundary/db/core.clj
@@ -1,7 +1,11 @@
 (ns minimal-api-lein.boundary.db.core
   (:require
+   [clojure.spec.alpha :as s]
    [clojure.string :as str]
    [integrant.core :as ig]))

+(s/def ::data any?)
+(s/def ::db (s/keys :req-un [::data]))
+
 (defrecord Boundary [data])

 (defmethod ig/init-key ::db [_ _]
```

すると、例えば `minimal-api-lein.boundary.db.todo/find-todos` は `:minimal-api-lein.boundary.db.core/db` を受け取って `::id` をキー、 `::todo` を値とするマップを返す関数として仕様を記述することができます。

また、 `minimal-api-lein.boundary.db.todo/find-todo-by-id` は `minimal-api-lein.boundary.db.core/db` と `::id` を受け取って `::todo` または `nil` を返す関数として仕様を表現できるでしょう。

```diff:minimal-api-lein/src/minimal_api_lein/boundary/db/todo.clj
@@ -1,5 +1,35 @@
 (ns minimal-api-lein.boundary.db.todo
   (:require
+   [clojure.spec.alpha :as s]
    [minimal-api-lein.boundary.db.core :as db]))
+
+(s/def ::id string?)
+(s/def ::todo (s/map-of string? string?))
+(s/def ::todos (s/map-of ::id ::todo))
+
+(s/fdef find-todos
+  :args (s/cat :db ::db/db)
+  :ret ::todos)
+
+(s/fdef find-todo-by-id
+  :args (s/cat :db ::db/db
+               :id ::id)
+  :ret (s/nilable ::todo))
+
+(s/fdef create-todo!
+  :args (s/cat :db ::db/db
+               :todo ::todo)
+  :ret ::id)
+
+(s/fdef update-todo!
+  :args (s/cat :db ::db/db
+               :id ::id
+               :todo ::todo)
+  :ret any?)
+
+(s/fdef delete-todo!
+  :args (s/cat :db ::db/db
+               :id ::id)
+  :ret any?)

 (defprotocol Todo
   (find-todos [db])
```

このように `Todo` プロトコル関数の入出力の仕様をclojure.specで定義することができます。

## 開発環境への組み込みと動作確認

specが定義できたので、開発環境でREPLからspecのチェックが動作することを確かめてみましょう。

ここで、Clojure標準ライブラリの `clojure.spec.test.alpha/instrument` 関数は関数のspecのうち引数に対するspec (`:args` spec)のみチェックを有効化するようになっている(cf. [spec Guide > Instrumentation](https://clojure.org/guides/spec#_instrumentation))ので、今回は戻り値に対するspec (`:ret` spec)も有効化できるサードパーティライブラリ[Orchestra](https://github.com/jeaye/orchestra)の `orchestra.spec.test/instrument` 関数を利用します。

```diff:minimal-api-lein/project.clj
@@ -18,6 +18,7 @@
                    :resource-paths ["dev/resources"]
                    :dependencies [[clj-http "3.11.0"]
                                   [integrant/repl "0.3.2"]
+                                  [orchestra "2020.09.18-1"]
                                   [pjstadig/humane-test-output "0.10.0"]]
                    :plugins [[jonase/eastwood "0.3.12"]
                              [lein-cloverage "1.2.1"]
```

```diff:minimal-api-clj.core/deps.edn
@@ -10,7 +10,8 @@
  :aliases
  {:dev {:extra-paths ["dev/resources" "dev/src"]
         :extra-deps {clj-http {:mvn/version "3.11.0"}
-                     integrant/repl {:mvn/version "0.3.2"}}}
+                     integrant/repl {:mvn/version "0.3.2"}
+                     orchestra {:mvn/version "2020.09.18-1"}}}
   :test {:extra-paths ["test"]
          :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"}}}
   :runner
```

関数のspecの組み込みはREPLでの `reset` (システム(再)起動 + リロード)のたびに実行できると便利なので、以下のように `dev` 名前空間で `integrant.repl/reset` の直後に `orchestra.spec.test/instrument` を呼び出す同名の関数 `reset` を定義してみました。

また、(`.gitignore` でバージョン管理対象外にする想定の) `local.clj` という名前のファイルがあれば `load` する機能を追加しています。

これにより、他の開発者に影響を与えることなく `dev` 名前空間に自分だけの独自ユーティリティを追加することができます([Ductテンプレートの `dev.clj`](https://github.com/duct-framework/duct/blob/0.12.1/lein-template/resources/leiningen/new/duct/base/dev.clj#L28-L29) でも使われている工夫です)。

```diff:minimal-api-lein/dev/src/dev.clj
@@ -6,8 +6,9 @@
    [clojure.test :as test]
    [clojure.tools.namespace.repl :refer [refresh]]
    [integrant.core :as ig]
-   [integrant.repl :refer [clear halt go init prep reset]]
-   [integrant.repl.state :refer [config system]]))
+   [integrant.repl :refer [clear halt go init prep]]
+   [integrant.repl.state :refer [config system]]
+   [orchestra.spec.test :as stest]))

 (defn read-config []
   (-> (io/resource "config.edn")
@@ -15,6 +16,11 @@
       ig/read-string
       (doto ig/load-namespaces)))

+(defn reset []
+  (let [result (integrant.repl/reset)]
+    (with-out-str (stest/instrument))
+    result))
+
 (integrant.repl/set-prep! (comp ig/prep read-config))

 (defn test
@@ -26,3 +32,6 @@

 (defn db-run [f & args]
   (apply f (db) args))
+
+(when (io/resource "local.clj")
+  (load "local"))
```

Emacs/Spacemacsユーザで前述の `cider-ns-refresh` を利用している場合には、 `dev/reset` 関数と同等の振る舞いになるように `cider-ns-refresh` の後に呼び出される関数を差し替えておくと良いでしょう。

早速 `local.clj` に次のような `integrant.repl/resume` の後に `orchestra.spec.test/instrument` する関数を `local.clj` ファイルに定義し、 `.dir-locals.el` の `cider-ns-refresh-after-fn` に登録します。

```clj:minimal-api-lein/dev/src/local.clj
(defn resume-instrument []
  (let [result (integrant.repl/resume)]
    (with-out-str (stest/instrument))
    result))
```

```emacs-lisp:minimal-api-lein/.dir-locals.el
((nil . ((cider-ns-refresh-before-fn . "integrant.repl/suspend")
         (cider-ns-refresh-after-fn  . "dev/resume-instrument"))))
```

REPLで `(reset)` してから再び `db-run` 関数でDBバウンダリ関数を動作確認してみると、想定外の引数に対しては関数の本体処理が実行されることなくspecエラーになることが分かります(ここでは確認しませんが、Orchestraの `instrument` を利用しているので戻り値が想定外の場合にもspecエラーになるでしょう)。

```clj
dev=> (db-run minimal-api-lein.boundary.db.todo/find-todo-by-id "todo2")
{"task" "?????"}
dev=> (db-run minimal-api-lein.boundary.db.todo/find-todo-by-id 2)
Execution error - invalid arguments to orchestra.spec.test/spec-checking-fn$conform! at (test.cljc:115).
2 - failed: string? at: [:args :id] spec: :minimal-api-lein.boundary.db.todo/id

dev=> (db-run minimal-api-lein.boundary.db.todo/create-todo! {"task" "something new"})
"todo4"
dev=> (db-run minimal-api-lein.boundary.db.todo/create-todo! ["task" "something new"])
Execution error - invalid arguments to orchestra.spec.test/spec-checking-fn$conform! at (test.cljc:115).
["task" "something new"] - failed: map? at: [:args :todo] spec: :minimal-api-lein.boundary.db.todo/todo
```

## テスト環境への組み込み

ローカル開発環境でspecのチェックが効くようになったので、最後にテスト時にも同様のチェックが有効になるようにしましょう。

今回は `clojure.test/use-fixtures` を利用して、全テストケースの前に1回だけ `orchestra.spec.test/instrument` を呼び出すようにしてみました。

```diff:minimal-api-lein/test/minimal_api_lein/test_helper.clj
@@ -1,7 +1,8 @@
 (ns minimal-api-lein.test-helper
   (:require
    [clj-http.client :as client]
    [clojure.java.io :as io]
-   [integrant.core :as ig]))
+   [integrant.core :as ig]
+   [orchestra.spec.test :as stest]))

 (defn test-config []
   (-> (io/resource "config.edn")
@@ -15,6 +16,12 @@

 (def test-url-prefix "http://localhost:3001")

+;;; fixtures
+
+(defn instrument-specs [f]
+  (stest/instrument)
+  (f))
+
 ;;; macros for testing context

 (defmacro with-system [[bound-sym binding-expr] & body]
```

```diff:minimal-api-lein/test/minimal_api_lein/core_test.clj
@@ -3,6 +3,10 @@
    [clojure.test :as t]
    [minimal-api-lein.test-helper :as helper :refer [with-db-data with-system]]))

+(t/use-fixtures
+  :once
+  helper/instrument-specs)
+
 (t/deftest test-api
   (with-system [sys (helper/test-system)]
     (with-db-data [sys {:todo {"todo1" {"task" "build an API"}
```

# まとめ

本記事では、1ファイルによるミニマルなREST APIを本格的な開発にも十分に耐えられる構成に段階的にリファクタリングしました。

- 依存ライブラリの更新を確認する
- Integrant-REPLでIntegrantベースのシステムのREPL開発フローを快適にする
- REST APIに対するテストとCIを整備する
- 名前空間を責務に合わせて分割し設定を外部化する
- "boundary"プロトコルで外部サービスアクセスを抽象化する
- clojure.specで関数の入出力の仕様を表現する

Clojureで開発するシステムを大規模化や複雑化に応えられるアーキテクチャに改善していくためのアプローチとして参考になれば幸いです。

REPLによるフィードバックをこまめに受け取りながら大きく複雑な問題にも立ち向かえるClojureでのプログラミングを楽しみましょう＞ω＜/

# Further Reading

- [Integrant](https://github.com/weavejester/integrant)
    - [Integrant-REPl](https://github.com/weavejester/integrant-repl)

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

- [clojure.spec](https://clojure.org/guides/spec)

- [Clojureで快適なREPL駆動開発のために"reloaded workflow"を実践しよう - Qiita](https://qiita.com/lagenorhynque/items/a752ffae490e83ebe70d)

- [『3つのLisp 3つの世界』](https://paren-holic.booth.pm/items/1317263)
    - Clojure編: Clojureによるデータ指向DSLを駆使したREST API開発

- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門 - Qiita](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)

- [Clojure/ClojureScript関連リンク集 - Qiita](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba)