はじめに
この記事はユーザベースアドベントカレンダーの24日の記事になります。
こんにちは。talol-rogersです。Clojureを使ってWebアプリケーションの開発を始めてから、半年が経ちました。特に最近携わっているプロジェクトではClojureを書く機会が多く、ちょっとしたスクリプトはClojure CLIで、APIはDuctで開発しています。半年前からDuctを使って開発していたのですが、10月にLeiningen templateが非推奨になったことをきっかけにClojure CLI + deps.ednでのDuct開発を行うようになり、自身の勉強も兼ねてこの記事を執筆することにしました。
Ductとは
DuctはClojureでサーバーサイドアプリケーションを開発するためのフレームワークです。汎用的な用途にも利用できますが、特にWebアプリケーションの開発に適しています。
Leiningen templateで始めるDuct開発
以前は、Leiningen templateを使って以下のコマンドでプロジェクトを作成していました。
$ lein new duct project +api +ataraxy +postgres
leiningen templateを使ってプロジェクトを作成した際の構成は以下のようになります。
.
├── README.md
├── dev
│ ├── resources
│ │ └── dev.edn
│ └── src
│ ├── dev.clj
│ └── user.clj
├── project.clj
├── resources
│ └── project
│ ├── config.edn
│ └── public
├── src
│ ├── duct_hierarchy.edn
│ └── project
│ ├── handler
│ └── main.clj
└── test
└── project
└── handler
開発者にとって主要となるのはproject.cljとconfig.ednです。
project.cljには、依存関係、ミドルウェア、プロファイルの設定などが書かれており、LeiningenベースのDuctプロジェクト全体の「ビルド設定の中心」にあたるファイルになります。
config.ednはDuct/Integrantが読み込むアプリケーション構成のメインとなるEDNファイルになります。ここでは、ハンドラやDBなどのキーと設定が定義されます。Ductはこのconfig.ednを読み込み、Integrantに渡してシステムを組み立てます。
ただ、公式ドキュメントにもあるように、Leiningenテンプレートは非推奨になっており、現在はClojure CLI + deps.ednでDuctプロジェクトを作成することが推奨されています。(Leiningenテンプレートは非推奨ではありますが、現在も利用自体は可能です)
Clojure CLI + deps.ednを使ったDuct開発
まずは、clojure commandを使用するために、Clojure CLIをインストールします。次に、プロジェクトの対象となるディレクトリを作成します。
$ mkdir project && cd project
curlを使って最小限のdeps.ednをダウンロードします。
$ curl -O duct.now/deps.edn
{:deps {org.clojure/clojure {:mvn/version "1.12.3"}
org.duct-framework/main {:mvn/version "0.4.3"}}
:aliases {:duct {:main-opts ["-m" "duct.main"]}}}
何度も使うらしいので、エイリアスを作成します。
$ alias duct="clojure -M:duct"
最後に、duct.ednを生成してセットアップは終了です。
$ duct --setup :duct
{:system {}}
開発者にとって主要になるのはdeps.ednとduct.ednです。とは言ってもdep.ednとduct.ednしかないのですが(笑)
deps.ednにはDuctを実行するための依存関係、設定のエイリアスを書きます。
- :depsに記載されたライブラリをMaven等から依存解決・ロード
- :aliasesに:ductや:testなどを定義し、clojure -M:duct —mainのようにCLIからDuctを起動
duct.ednはリポジトリのルート直下に置く必要があり、 Duct 専用の「アプリケーション構成ファイル」で、Integrant コンポーネントや Duct モジュールをデータとして宣言します。
つまり、deps.ednが「Duct Mainを動かす環境」を整備し、duct.ednが「Duct Mainが読み込むアプリケーションの中身」を定義するという分担となっています。
また、Leiningen templateで作成したprojectとざっくり対応させると
deps.edn ≒ project.clj
どちらもJVM起動時にクラスパスに何を載せて、どのように実行するかを決めるレイヤー
duct.edn ≒ config.edn + dev.edn等の環境別設定
どちらもDuctの構成そのもの(Integrantの設定)を宣言するレイヤー
といった感じでしょうか。
Leiningen templateとClojure CLI + deps.ednの違い
Leiningen templateからClojure CLI + deps.ednを使うようになっていくつか違いを発見したので紹介します。
REPLからシステム起動方法
-
Leiningen templateの場合
(dev) → (go)で起動する必要がありました。
まずREPL起動直後はuser nsが選ばれます。user ns内に定義されている(dev)を実行するとdev nsにスイッチし、dev nsからIntegrantの(go)が呼び出され、システムが起動するという二段階となっていました。(ns user) (defn dev "Load and switch to the 'dev' namespace." [] (require 'dev) (in-ns 'dev) :loaded)(ns dev (:require [clojure.repl :refer :all] [integrant.repl :refer [clear halt go init prep reset]])) (duct/load-hierarchy)
-
Clojure CLI + deps.ednの場合
(go)のみで起動可能になりました。
まず、下記のコマンドでREPLを立ち上げます。$ clojure -M:duct --nrepl --ciderこの際に、-M:ductで先程設定したdeps.edn内のductエイリアスが選択されます。
{:deps {org.clojure/clojure {:mvn/version "1.12.3"} org.duct-framework/main {:mvn/version "0.4.3"}} :aliases {:duct {:main-opts ["-m" "duct.main"]}}}duct.main/-mainが呼ばれ、オプションで —nreplを付けているため、start-nrepl関数が実行されます。
(ns duct.main) (defn- start-nrepl [options] (term/with-spinner (if (:cider options) " Starting nREPL server with CIDER" " Starting nREPL server") ((requiring-resolve 'duct.main.nrepl/start-nrepl) load-config options))) (defn -main [& args] (let [config (load-config) opts (cli/parse-opts args (cli-options (:vars config))) options (:options opts)] (binding [term/*verbose* (-> opts :options :verbose)] (term/verbose "Loaded configuration from: duct.edn") (cond (:help options) (print-help opts) (:show options) (show-config config options) :else (do (when (->> options keys (filter #{:main :repl :test}) next) (term/printerr "Can only use one of the following options:" "--main, --repl, --test.") (System/exit 1)) (when (or (:main options) (:repl options) (:nrepl options) (:test options)) (setup-hashp options)) (when (:setup options) (setup (:setup options))) (when (:nrepl options) (start-nrepl options)) (cond (:main options) (init-config config options) (:test options) (start-tests options) (:repl options) (start-repl options) (:nrepl options) (.join (Thread/currentThread)) (:setup options) (shutdown-agents) :else (print-help opts)))))))次に、start-nrepl関数からduct.main.nrepl/start-nreplが呼ばれ、さらにこの関数内でduct.main.user/setup-user-nsが呼ばれます。
(ns duct.main.nrepl (:require [duct.main.user :as user]) (defn start-nrepl [load-config options] (let [server (nrepl/start-server {:handler (nrepl-handler options)})] (println "nREPL server started on port" (:port server)) (cli/save-port-file server {}) (doto server stop-nrepl-on-shutdown) (when-not (:main options) (user/setup-user-ns load-config options))))最終的に、setup-user-ns関数内でuser nsにスイッチし、Intergrant.replでreferされているgoを実行し、システムが起動するという仕組みになっています。
(ns duct.main.user (:require [duct.main.config :as config] [integrant.core :as ig] [integrant.repl :as igrepl]) (defn setup-user-ns [load-config options] (in-ns 'user) (require '[clojure.repl :refer [apropos dir find-doc pst source]]) (require '[clojure.repl.deps :refer [sync-deps]]) (require '[duct.main.doc :refer [doc]]) (require '[integrant.repl :refer [clear go halt prep init reset reset-all]]) (require '[integrant.repl.state :refer [config system]]) (ig/load-hierarchy) (ig/load-annotations) (igrepl/set-prep! #(-> (load-config) (config/prep options) (doto ig/load-namespaces))) (.addShutdownHook (Runtime/getRuntime) (Thread. igrepl/halt)))
環境変数を受け取り注入するとき
以前(Leiningen templatesを使っていたとき)は、環境変数を以下のように受け取って注入してました。
:project/execute
{:hoge-api-url #duct/env ["HOGE_API_URL" :or "http://localhost:3100"]}
Clojure CLI + deps.ednに変えてからは以下のように環境変数を受け取って注入するようになりました。
{:vars {hoge-api-url {:env "HOGE_API_URL"
:default "http://localhost:3100"}}
:project/execute
{:hoge-api-url #ig/var hoge-api-url}
Ductの公式documentでも後者のvarsを使って、環境変数を受け取るやり方を推奨しています。
varsは#duct/envが扱えるenv, type, defaultに加えて、argとdocも扱えるため、特段理由がない場合はvarsを使うのが良いのかなと思っています。また、複数のnsに外部からの注入が必要な場合は、varsで一元管理できるのもvarsを使うメリットの1つだと思います。
:arg
a command-line argument to take the var’s value from
:default
the default value if the var is not set
:doc
a description of what the var is for
:env
an environment variable to take the var’s value from
:type
a data type to coerce the var into (one of: :str, :int or float)
Routingの違い
-
Leiningen templateの場合
先程例で述べたように、オプションとして +ataraxyが用意されていたので、標準はataraxyだったかと思います。
実装例は以下です
{:duct.module/ataraxy {[:get "/"] [:index] [:get "/example"] [:example]}} -
Clojure CLI + deps.ednの場合
公式のチュートリアルでは、Reititを例に紹介しています。
The router component uses Reitit, a popular data-driven routing library for Clojure. Other routing libreries can be used, but for this documentation we’ll use the default.
実装例は以下です
{:duct.module/web {:routes [["/" {:get :todo.routes/index}]]}}
おわり
いかがでしょうか。DuctがLeiningen templateを非推奨にしたことで、これからClojure CLI + deps.ednがDuctプロジェクトのデファクトスタンダードになっていくと思います。ただ、内部では両者ともIntegrantを使用しており、大きく使い勝手が変わることはないと思います。当記事がみなさんの開発の役に立てれば幸いです。