Part3: Compojure とはなんなのか

まずは簡単なルーティング機能を実装する

Web アプリケーションに必要なもののひとつにルーティング機能があります。前の Part までで Hello, world が出来るようになりましたが、このままでは /todo/new という URI にアクセスしたら TODO を新規作成する画面を表示するようにしたいという要望に対応出来ません。そこでまずは特定の URI に対して適切なコンテンツを返すことことをまずは特別なライブラリを使わずに実装してみます。

最初にホーム画面と TODO の一覧画面を表示するために以下のルーティングを定義しようと思います。

{"/"     home
 "/todo" todo-index}

http://localhost:3000/ でホーム画面を表示して、 http://localhost:3000/todo で TODO の一覧を表示するだけの簡単なものです。

早速上記のマップを定義してみましょう。ファイルは src/todo_clj/core.clj です。

;; src/todo_clj/core.clj
(def routes
  {"/" home
   "/todo" todo-index})

これを評価すると当然エラーが出るので home 関数と todo-index 関数を実装しましょう。

;; src/todo_clj/core.clj
(defn ok [body]
  {:status 200
   :body body})

(defn html [res]
  (assoc res :headers {"Content-Type" "text/html; charset=utf-8"}))

(defn not-found []
  {:status 404
   :body "<h1>404 page not found</1>"})

(defn home-view [req]
  "<h1>ホーム画面</h1>
   <a href=\"/todo\">TODO 一覧</a>")

(defn home [req]
  (-> (home-view req)
      ok
      html))

(def todo-list
  [{:title "朝ごはんを作る"}
   {:title "燃えるゴミを出す"}
   {:title "卵を買って帰る"}
   {:title "お風呂を洗う"}])

(defn todo-index-view [req]
  `("<h1>TODO 一覧</h1>"
    "<ul>"
    ~@(for [{:keys [title]} todo-list]
        (str "<li>" title "</li>"))
    "</ul>"))

(defn todo-index [req]
  (-> (todo-index-view req)
      ok
      html))

一気に色んな関数が増えましたが、よくよく見ると共通的な処理をまとめているだけなのが分かると思います。そして、 routes をエラーなく評価することが出来るようになりました。まだデータベースを使うことが出来ないので、ここでは単純なマップのベクタを todo-list という名前で定義しておきます。

これらの関数は独立しているのでテストするのが容易です。ファイルを保存してロードした後に(もしくは全ての関数を評価した後に) REPL 上で次のようなコードを試してみましょう。

user> (in-ns 'todo-clj.core)
;; => #object[clojure.lang.Namespace 0x121aaddc "todo-clj.core"]
todo-clj.core> (home {})
;; => {:status 200, :body "<h1>ホーム画面</h1>\n   <a href=\"/todo\">TODO 一覧</a>", :headers {"Content-Type" "text/html; charset=utf-8"}}

ネームスペースを切り替えた後 [1] [2](home {}) を評価することでレスポンスマップを手に入れることが出来ました。 home 関数へと渡している空のマップはリクエストマップですが、これは home 関数が今回は内部で他のパラメーターを使わないためこのように空を渡しています。

あとはこれを handler 関数から呼び出せるようにするだけです。残りの関数を書いてみましょう。

;; src/todo_clj/core.clj
(defn match-route [uri]
  (get routes uri))

(defn handler [req]
  (let [uri (:uri req)
        maybe-fn (match-route uri)]
    (if maybe-fn
      (maybe-fn req)
      (not-found))))

最終的にこんな感じになりました。 match-route 関数を新しく作り、 handler 関数を修正しました。これも先ほどと同様に以下のように REPL 上でテスト出来ます。

todo-clj.core> (handler {})
;; => {:status 404, :body "<h1>404 page not found</1>"}
todo-clj.core> (handler {:uri "/"})
;; => {:status 200, :body "<h1>ホーム画面</h1>\n   <a href=\"/todo\">TODO 一覧</a>", :headers {"Content-Type" "text/html; charset=utf-8"}}
todo-clj.core> (handler {:uri "/todo"})
;; => {:status 200, :body ("<h1>TODO 一覧</h1>" "<ul>" "<li>朝ごはんを作る</li>" "<li>燃えるゴミを出す</li>" "<li>卵を買って帰る</li>" "<li>お風呂を洗う</li>" "</ul>"), :headers {"Content-Type" "text/html; charset=utf-8"}}

Part2 までで作成しているサーバーを起動して実際に http://localhost:3000/todo へとアクセスすることでも結果が確認出来ます。こんな感じでここまででルーティング機能を独自で実装してきたわけですが、このままアプリケーションを作り続けていくにはちょっと機能が色々と足りませんし、それらを実装してしまうのは骨が折れます。具体的にはここまでで実装したものだけでは POST メソッドに対応出来ませんし、 /user/:id というようなマッチングを行うことが出来ません。

なのでもっと便利なものを使いたいと思います。それが後述する Compojure になります。

[1]余談ですがネームスペースの切り替えは各エディタのプラグインなどで実装されているため in-ns を使わなくても簡単に出来ます。 Cider では M-x cider-repl-set-ns 、 Cursive では Switch REPL NS to current file で実行出来ます。
[2]もうひとつ大事なことですが、ファイルに書いたものをロードすることと REPL 上で関数を定義するのは同じ意味なので、ファイル (src/todo_clj/core.clj) 上で (home {}) を評価するのは同じ意味になります(エディタのプラグインによってネームスペースを切り替える必要があったりなかったりするのでそこは注意が必要です)。

Compojure ってなんでしょう

前の方でルーティング機能を実装したので流れで分かるとは思いますが Compojure はルーティング機能を提供するシンプルなライブラリです。一般的に何故か Web フレームワークという風に認知されていますが、主にルーティングのためのみに使用するライブラリとなります [3]

Compojure を導入することで今まで無理やり書いていたルーティングがよりシンプルになります。具体的には次のように書くことが出来るようになります。

(defroutes handler
  (GET "/" req home)
  (GET "/todo" req todo-index))

比較的 Rails などに近い DSL なのでそちらを知っていれば馴染みやすいでしょう。

この他にも Clojure のルーティングライブラリは以下のように沢山あるのですが (2015 年時点)、今回は特にこだわりがないので広く一般的に認知されている Compojure を使っていきたいと思います [4]

  • Compojure
  • Moustache
  • RouteOne
  • Pedestal
  • gudu
  • secretary
  • silk
  • fnhouse
  • bidi

余談にはなりますが、ルーティングライブラリにはマクロで書かれた DSL を使って実現するものと、マップやベクタなどでデータを定義しておいて実現するものと主に 2 種類あります。前者の方法で実現しているのが Compojure で、後者の方法は今回ここまでで独自に定義したようなものですね(実用性の高いライブラリではもうちょっと綺麗に定義出来るんですが)。このように同じような機能を提供してくれるライブラリも沢山あるので好みに応じて好きなものを選んでいけるようになるのがいいでしょう。

また Compojure の提供するネームスペースについても簡単に説明しておきましょう。

  • compojure.coercions: ルートパラメーター、つまり GET リクエストのパラメーターの型を String から強制するさいに使える関数を提供します。
  • compojure.core: Compojure の基礎となる部分でルーティングに関する幾つかのマクロを提供します。
  • compojure.handler: 廃止予定。ここにあったものは現在では Ring-Defaults という別のライブラリになって提供されています。
  • compojure.response: レスポンスマップの :body には通常 4 つの型しか使えませんが、このネームスペースで定義されている render 関数を通すことで他の型を通すことが出来るようになるのですが、基本的にこのネームスペースは意識して使う必要はありません。
  • compojure.route: 幾つかのよく使うレスポンスを返す関数を提供します。

主に使うのは core と route ですが、 coercions なども使うことが出来ます。

[3]この記事 で作者が “Compojure is a small web framework based on Ring” と言っていますが、既に Compojure の README からも web framework という表記が消されているので無視していいでしょう。
[4]私が好きなのは JUXT の作っている bidi というライブラリです。

Compojure を導入する

まずは Compojure を依存性に追加する

Ring を追加したときのように project.clj へと依存性を追加します。

:dependencies [[org.clojure/clojure "1.7.0"]
               [ring "1.4.0"]
               [compojure "1.4.0"]]

こんな感じで追加したら一度 REPL を再起動しましょう。そうすると自動的に Leiningen が REPL を起動する前に依存性を解決してくれます(丁寧にやるなら lein deps などのコマンドを使った後に REPL を起動します)。

Compojure でルーティングを書き換える

次に Compojure を使って今のコードを書き換えてみます。

;; src/todo_clj/core.clj
(ns todo-clj.core
  (:require [compojure.core :refer [defroutes context GET]]
            [compojure.route :as route]
            [ring.adapter.jetty :as server]
            [ring.util.response :as res]))

まずは ns マクロの :require 部分に Compojure を追加します。 :refer:as の使い分けを何処でしているのか分かりにくいかもしれませんが、 defroutesGET のようなマクロはネームスペース内で衝突し難いですし、使うときにネームスペースの指定をせずに使えたほうが簡単でいいので :refer を使っています。勿論、 [compojure.core :as c] として (c/defroutes hoge ...) と書いても間違いではないです。ちなみに今回ついでに ring.util.response も加えています。これについては後述します。

次に handler 関数を再定義しましょう。

;; src/todo_clj/core.clj
(defroutes handler
  (GET "/" req home)
  (GET "/todo" req todo-index)
  (route/not-found "<h1>404 page not found</h1>"))

これを再評価して REPL からサーバーを起動して確認してみましょう。すると今まで通り、ホーム画面や TODO 一覧画面が表示されているのが確認出来ると思います。 defroutesdefdefn と似ていますが、第一引数にハンドラ名となるシンボルを受け取り、第二引数以降にルート定義を受け取ります。ルート定義は主に GET , POST などの compojure.core にあるマクロを使いますが、その他にも not-found のような compojure.route の関数なども使うことが出来ます。

それから ok, not-found 関数を削除し、 html 関数も少々書き換えます。

;; src/todo_clj/core.clj
(defn html [res]
  (res/content-type res "text/html; charset=utf-8"))

ring.util.response には幾つかのレスポンスマップを操作する便利な関数が定義されているためこれを利用することにしました。 ring.util.response/content-type 関数はレスポンスマップとヘッダーの Context-Type に設定するバリューを受け取り、レスポンスマップのヘッダーの "Context-Type" キーに受け取ったバリューを設定するという簡単なものです。前の html 関数のように自分でキーを設定してもいいのですが、このように既にある関数を利用できるのであれば使った方がいいでしょう。

また ok 関数を削除したので home, todo-index 関数にも多少の修正が必要となります。

;; src/todo_clj/core.clj
(defn home [req]
  (-> (home-view req)
      res/response
      html))

(defn todo-index [req]
  (-> (todo-index-view req)
      res/response
      html))

ok 関数の代わりに ring.util.response/response を使うことにしました。 ring.util.response/response は前に書いた ok 関数に似ているものですが、これは ok 関数と同じように body を受け取りレスポンスマップを生成するというシンプルなものですね。

Compojure についてもう少し詳しく知る

ここまでで Compojure を使ってコードを書き換えてきましたが、もう少し Compojure が何を出来るのかを説明したいと思います。その後に今回作る TODO アプリの骨格となるルーティングの定義をもう少し行いましょう。

今まで見てきたように Compojure でのルート定義は以下のようになります。

(GET "/" req home)

このようなルート定義はリクエストマップを受け取りレスポンスマップを返す Ring ハンドラーを返します。この Ring ハンドラーを実行出来るかというのは HTTP メソッドとパスの定義によって決まります。この例では HTTP メソッドが GET でパスが "/" のときのみ実行されるということが分かります。また実行できない場合、ルート定義は nil を返します。

compojure.core ネームスペースには GETPOST というマクロがあると書きましたが、これらは Ring が扱える HTTP メソッドと同名のマクロがあります。なので、実際に使えるものとしては GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD があり、どの HTTP メソッドでも良いという場合には ANY マクロを使うことができます。

GET などのマクロは 2 つ以上の引数を受け取ります。第一引数はパスで、第二引数はバインディング、第三引数以降ではバインディングを利用して返却するレスポンスを作る部分になります。

パスは文字列で定義でき "/""/todo" などと定義するのですが、 "/todo/:id" などといったルートパラメーターを含めた特殊な指定も出来ます。このように指定することで次の "/" もしくは "." まで :id の部分にどのような文字列でもパスとして受け入れることができるようになります。ただ、これでは数字だけを使いたいなどというときに少々不便です。 Compojure ではその問題を解決するために指定できる文字を正規表現によって次のように制限することができます。

;; todo-show はまだ未定義の架空の関数です
(GET ["/todo/:id" :id #"[0-9]+"] req todo-show)

バインディングの機能については Clojure の let などで使える分配束縛と似たような機能が提供されていると考えてもらえるといいと思います。今回は分配束縛を使っていませんが使うことで多少楽にルート定義をすることが出来ます。例えば次のように :params を簡単にリクエストマップから取り出すことが出来ます。

(GET ["/todo/:id" :id #"[0-9]+"] {params :params} (todo-show params))

なれないと分かり難いかもしれませんが、 let の左辺を {params :params} として右辺にはリクエストマップがきていると思えば理解がしやすいと思います(実際にそういう風にマクロが展開されています)。 また次の例は Compojure の中でも特徴的なものですが、パスの中で :id などのルートパラメーターを定義している場合、それを簡単に取り出すことが出来るようになっています。

(GET ["/todo/:id" :id #"[0-9]+"] [id] (todo-show id))

このようにベクターの中でルートパラメーターを直接指定することで、それを簡単に抜き出し利用することが出来ます。これだけだと id はただの文字列ですが、これを数値に変換することも Compojure では出来ます。

(ns todo-clj.core
  (:require [compojure.coercions :refer [as-int]] ;; これを追加していると…
            [compojure.core :refer [defroutes context GET]]
            [compojure.route :as route]
            [ring.adapter.jetty :as server]
            [ring.util.response :as res]))

(GET ["/todo/:id" :id #"[0-9]+"] [id :<< as-int] (todo-show id))

ちょっと複雑ですね。とはいえこのようにルートパラメーターを簡単に展開出来るのは便利なこともあるので使ってみてもいいかもしれません。

次に GET などのマクロの第三引数にあたる部分について説明しようと思います。ここまでの例では第三引数に対して hometodo-index というような関数だけを渡していました。実はここには関数以外にも文字列やマップなどを渡すことが出来ます。

(GET "/" req (home-view req))

これは home 関数の中で呼び出されていた home-view 関数をルート定義の中で呼び出して実行し、文字列を返すようにしています。このように書いても今までと同様にホーム画面を表示することが出来ます。これは Compojure が内部的に compojure.response ネームスペースで定義されている render 関数を呼び出していて、文字列型が渡されたときに自動的にレスポンスマップを生成し返すようになっているからです。この第三引数部分では第二引数部分で束縛しておいたパラメーターを利用することが出来るので分配束縛と組み合わせて何かをしたいときには便利です。ただし、関数を直接渡した場合は第二引数部分で束縛しておいたパラメーターは使うことが出来ず、その関数には元々のリクエストマップが直接渡されます。

;; id を利用したい関数
(defn todo-show [id]
  (prn-str id))

(defroutes bad?-handler
  (GET "/todo/:id" [id] todo-show)) ;; id を渡したいがリクエストマップが直接渡される

(defroutes good-handler
  (GET "/todo/:id" [id] (todo-show id))) ;; このように関数を実行すれば分配束縛した id が利用出来る

そしてルート定義は routes 関数でまとめることが出来ます。 routes 関数はそれぞれのルート定義(ハンドラー)をひとつの Ring ハンドラーへとする役割を持っています。

(def handler
  (routes ;; compojure.core/routes
   (GET "/" req home)
   (GET "/todo" req todo-index)
   (GET "/todo/:id" [id] (todo-show id))))

それぞれのルート定義は上から順番に解決出来るかが試行され、 nil を返さないルート定義を探します。またルート定義をまとめるこのパターンは一般的なので defroutes マクロが提供されます(ここまでで既に使っていますが)。 routes 関数は routes 関数でまとめたハンドラーを含めることが可能なので次のような定義も可能です。

(defroutes main-routes
  (GET "/" req home)
  (route/not-found "<h1>404 page not found</h1>"))

(defroutes todo-routes
  (context "/todo" req
    (GET "/" req todo-index)
    (GET "/new" req todo-new)
    (context "/:id" [id]
      (GET "/" req (todo-show id)))))

(defroutes handler
  (routes
   todo-routes
   main-routes)) ;; main-routes には絶対に nil でない値を返す not-found が使われているので、順番を意識する必要がある

context という関数が新しく登場していますが、 GET マクロなどのパスの共通部分をまとめるものです。これは GET などのマクロと同じように第一引数にパス、第二引数にバインディング、第三引数以降にはルート定義を並べます。

ここまでで Compojure の機能をひと通り紹介しましたが、今回紹介した中で今後使わない機能としては GET マクロなどの第二引数を使った分配束縛です。理由としてはこれを使うと少々煩雑になるのとシンプルにリクエストマップを渡す関数を第三引数に渡すようにしておくと後々ルーティングライブラリを変更する場合に楽だからです。今後第二引数は利用しないというのを明示するために _(アンダースコア) で潰していきますが、これを読んでいる方で使いたいという方はそこの部分を読み替えながら書いてみてください。

次はこれらを使って実際に Web アプリケーションの核となるルーティングを定義していきましょう。

ルーティング定義を行いアプリケーションの骨格をつくる

TODO アプリに必要なものはなんでしょう。まずは TODO を作成、編集、表示、削除、一覧表示、検索など出来ればいいですよね。

(defroutes main-routes
  (GET "/" _ home)
  (route/not-found "<h1>Not found</h1>"))

(defroutes todo-routes
  (context "/todo" _
    (GET "/" _ todo-index)
    (GET "/new" _ todo-new)
    (POST "/new" _ todo-new-post)
    (GET "/search" _ todo-search)
    (context "/:todo-id" _
      (GET "/" _ todo-show)
      (GET "/edit" _ todo-edit)
      (POST "/edit" _ todo-edit-post)
      (GET "/delete" _ todo-delete)
      (POST "/delete" _ todo-delete-post))))


(def app
  (routes
   todo-routes
   main-routes))

こんな感じの定義が出来れば良さそうですね。ですが、これを src/todo_clj/core.clj に全て定義していくのはファイルが大きくなりすぎるのでそろそろファイルをわけていきましょう。 src/todo_clj/handler というディレクトリを作ってそこに main.cljtodo.clj を作ります。さらに html 関数をいろんなところで使いたくなるので src/todo_clj/util/response.clj を作りましょう( core.cljhtml 関数を置いたままでも他のネームスペースから使えるんですが、循環参照が起きてしまうので違うネームスペースを作ったほうがいいんです)。

;; src/todo_clj/util/response.clj
(ns todo-clj.util.response
  (:require [ring.util.response :as res]))

(def response #'res/response)
(alter-meta! #'response #(merge % (meta #'res/response)))

(defn html [res]
  (res/content-type res "text/html; charset=utf-8"))

まずは src/todo_clj/util/response.clj を作成します。今まで使っていた html の定義はそのままに持ってきていますが、 ring.util.response/response もこのネームスペースに再定義します。再定義する際に ring.util.response/responseVartodo-clj.util.response/response に渡しているわけですが、このときにメタ情報が失われます(つまり引数の数などといった情報)。なので、 alter-meta! でメタ情報を更新しています。こうすることでハンドラー用のネームスペースは ring.util.responsetodo-clj.util.response のふたつを require しなくても ring.util.response のみを require すればよくなります(必要に応じて関数をここに再定義したり足せば不便はなくなります)。

次はアプリケーション本体のハンドラーを定義しましょう。主にホーム画面や 404 ページへリダイレクトさせるハンドラーを定義していくネームスペース、 todo-clj.handler.main を作成します。

;; src/todo_clj/handler/main.clj
(ns todo-clj.handler.main
  (:require [compojure.core :refer [defroutes GET]]
            [compojure.route :as route]
            [todo-clj.util.response :as res]))

(defn home-view [req]
  "<h1>ホーム画面</h1>
   <a href=\"/todo\">TODO 一覧</a>")

(defn home [req]
  (-> (home-view req)
      res/response
      res/html))

(defroutes main-routes
  (GET "/" _ home)
  (route/not-found "<h1>Not found</h1>"))

特筆すべきところはあまりないでしょう。どれも今までに書いてきたものの流用です。

次は TODO アプリのコア部分になるハンドラーを定義していきます。とは言っても最初なのでルーティングだけをずらずらと定義していきます。

;; src/todo_clj/handler/todo.clj
(ns todo-clj.handler.todo
  (:require [compojure.core :refer [defroutes context GET POST]]
            [todo-clj.util.response :as res]))

(def todo-list
  [{:title "朝ごはんを作る"}
   {:title "燃えるゴミを出す"}
   {:title "卵を買って帰る"}
   {:title "お風呂を洗う"}])

(defn todo-index-view [req]
  `("<h1>TODO 一覧</h1>"
    "<ul>"
    ~@(for [{:keys [title]} todo-list]
        (str "<li>" title "</li>"))
    "</ul>"))

(defn todo-index [req]
  (-> (todo-index-view req)
      res/response
      res/html))

(defn todo-new [req] "TODO new")
(defn todo-new-post [req] "TODO new post")
(defn todo-search [req] "TODO search")
(defn todo-show [req] "TODO show")
(defn todo-edit [req] "TODO edit")
(defn todo-edit-post [req] "TODO edit post")
(defn todo-delete [req] "TODO delete")
(defn todo-delete-post [req] "TODO delete post")

(defroutes todo-routes
  (context "/todo" _
    (GET "/" _ todo-index)
    (GET "/new" _ todo-new)
    (POST "/new" _ todo-new-post)
    (GET "/search" _ todo-search)
    (context "/:todo-id" _
      (GET "/" _ todo-show)
      (GET "/edit" _ todo-edit)
      (POST "/edit" _ todo-edit-post)
      (GET "/delete" _ todo-delete)
      (POST "/delete" _ todo-delete-post))))

TODO 一覧、新規作成、検索、表示、編集、それから削除機能までをカバーしたハンドラーとルーティングを定義しました。ハンドラーは幾つか新しく足していますが、これらは仮実装なので後ほど実装することにしましょう。

ここまでで今まで todo-clj.core に定義してあったもののほとんどを他のネームスペースに移動しました。綺麗になった todo-clj.core は次のようになります。

;; src/todo_clj/core.clj
(ns todo-clj.core
  (:require [compojure.core :refer [routes]]
            [ring.adapter.jetty :as server]
            [todo-clj.handler.main :refer [main-routes]]
            [todo-clj.handler.todo :refer [todo-routes]]))

(defonce server (atom nil))

(def app
  (routes
   todo-routes
   main-routes))

(defn start-server []
  (when-not @server
    (reset! server (server/run-jetty #'app {:port 3000 :join? false}))))

(defn stop-server []
  (when @server
    (.stop @server)
    (reset! server nil)))

(defn restart-server []
  (when @server
    (stop-server)
    (start-server)))

今まで handler として定義してあったものが app という名前になっています。この名前自体は慣習に近いのでそれに倣いますが、大凡ミドルウェアを適用した後のハンドラーに app と名付けることが多いようです。ここまでの内容はほとんどが書き直しでしたが、随分とスッキリしたんじゃないんでしょうか。ここまで書きなおした内容は実際に全てのファイルをロードして、ブラウザ上で確認してみてください。画面としてはホーム画面と TODO 一覧画面があり、不適当な URI を入力した場合 404 と表示されるだけで代わり映えはありませんが、 http://localhost:3000/todo/new などとブラウザで確認すると “TODO new” と表示されていると思います。アプリケーションぽくなってきた気がしますね。

この Part ではルーティングの定義に終始しましたが、次の Part では今まで文字列で HTML を表現していたのをやめてもっと簡単に HTML を表現出来るよう方法について学んでいきます。

ここまでで学んだこと

  • リクエストマップから URI の情報を取得して自分でルーティングを定義する方法
  • Compojure の機能全般について
  • Compojure での実際にルーティング定義方法