ring/compojureでサーバを始めてみる(入門編)

*遅れてしまって申し訳ない…*

# はじめに

Clojure でウェブアプリまたはウェブフレームワークといえば、まだ Clojure が 1.2 以前だった頃に ring という Servlet API の Wrappers & Protocols と、それを利用した軽量な~~フレームワーク~~ルーティングライブラリ(修正 2015/12/11)である Compojure 、そして、 Compojure を利用して作られた Conjure や Noir といったフルスタックなフレームワークが開発されましたが、Clojure 1.2 時点でのリリースを境に、どれも開発が止まっていました。
それが[今年になって、 Compojure の開発がようやく再開された](https://github.com/weavejester/compojure)ことで Clojure でウェブ開発する環境が再び整ったため、試しに手元にある環境で Twitter 連携サービスを作ろうと思い投稿を決定しました。

# 作業を始める前に

まずはハードの準備が必要ですね。環境は以下の通りです。

## 稼働環境

ホスト

* Arch Linux 64bit
* Intel Core i5-3570
* DDR3 DIMM(32GB)
* HDD 1TB(not RAID)

ゲスト1(今回記事に上げる鯖)

* Debian wheezy on VirtualBox
* メモリ 4GB
* CPU 4 cores / 最大使用率100%
* HDD 4GB(MBR(relatime)), 8GB(/var/log(relatime, noexec)), 4GB(/var/www(relatime))
* Ring & Compojure(Clojureのフレームワーク)

ゲスト2(データベースサーバ)

* Debian wheezy on VirtualBox
* メモリ 4GB
* CPU 4 cores / 最大使用率 100%
* HDD 4GB(MBR(relatime)), 8GB(/var/log(relatime, noexec)), 32GB(/var/db(relatime, noexec))

## サーバ構成

### どういうサービスか

**Twitterと連携して、取得したツイートを短期的に保存する**

Favstarのリプライ版だと思ってください。また、OAuth認証を通す・通さない、サービスへログインする・しないなど、セッションの状態でサービスを振り分ける事も検討しています。

VM上で動かすため、1台の物理マシンと1台の物理ルータですべてやりくりする必要があるのと、上述の通りデータベースサーバのHDDは高々32GBであることを考慮するとあまり大量のデータは保存できません。参考として、以下の情報を基準にサーバの仕様を考えます:

### データ量の見積もり

1. JSONをテキストとして見た場合、ツイート1つで送受信される容量は4キロバイト前後。  
2. ツイートのリミットは1日 2400件。([Twitterヘルプセンター](https://support.twitter.com/groups/50-welcome-to-twitter/topics/204-the-basics/articles/249071-twitter-apidm)より)
3. 人は、その日知り得た情報の内74%をその日の内に忘れ、26%を保持する。

1.について: [Apigee's API Console](https://dev.twitter.com/rest/tools/console)でstatuses/update.jsonを適当な内容のツイートと共に呼んでみて下さい。UTF-8では1文字のサイズは高々4バイトで、1度のツイートで送れるテキストは140文字。プロフィールに送れるテキストは160文字。この2つを合わせてもその容量は

300文字×4=1.2キロバイト

です。そして、ツイートとプロフィールを差し引きしたJSON(テキスト)の容量は2.6キロバイトです。

2.について: 本当に2400件みっちり送れば1人でも1日で約10Mの容量になってしまいます。
そこで、所謂"めっちょツイートしている(過去にしていたのも含む)"人が、1日にどれだけツイートしているのかを、以下の2つのサイトを参考にしてみてみましょう。

* [Twitter Counter(ヴェネティスさん)](http://twittercounter.com/VENETHIS)
* [Twilog(ヴェネティスさん)](http://twilog.org/VENETHIS)

どうやら単純なツイート数では1位の人が断トツ（？）でヴェネティスさんなので、彼を参考にしましょう。(軽く調べるとKeitai webで規制されなかった頃に"遊びまくってた"とか)
妙に飛び抜けちゃってるのを外すと、1日あたり(150+-50)件のようです。

続いて、ツイ廃アラートで日々ツイート数を教えてくれてる人からも一人、参考にしましょう。
Search APIで調べたところ、最も多い1日のツイート数は400〜600件のようなので、そこに出ているユーザの情報を見てみましょう。

* [Twitter検索「ポスト数：600」](https://twitter.com/search?src=typd&q=%E3%83%9D%E3%82%B9%E3%83%88%E6%95%B0%EF%BC%9A600)
* [Twilog(はるにょさん)](http://twilog.org/H_nyonyo/)

彼もまた、飛び抜けてる分を除けば1日約200ツイートということになります。

3.について: これは[エビングハウスの忘却曲線](http://ja.wikipedia.org/wiki/%E5%BF%98%E5%8D%B4%E6%9B%B2%E7%B7%9A)の受け売りです。人の記憶力がこの曲線に従うと決めておきます。すると、反復しなければ直近1ヶ月以上前の記憶をほとんど忘れていることになるので、これより多くの情報を保持する必要はなさそうです。

以上を踏まえれば、一人あたり1ヶ月分(200件*30日*4キロバイト=24メガバイト)を保存できれば十分ということになります。

# プロジェクトと依存ライブラリ

さて、それでは本題に入りましょう。まずは依存ライブラリを紹介します。

### Compojure

Clojureでおなじみのフレームワークです。当時これより後に出ていたフルスタックなフレームワークは、ほとんどこの Compojure をベースにして開発されています(今となってはほぼ過去の話ですが)。Compojure で管理できる部分は

1. URLのルーティング
2. Response(View)
3. Handler(関数)

の3つです。ただし、 Response には データ構造や言語の構文を解釈してHTMLに変換する ための機能はありません。あくまでも、文字列、マップ、関数、deref、ファイル、シーケンス、InputStream、URLと言った、Clojureでもよく使う クラス や データ構造 に対して、 render関数の実装を与えている だけです。

### Hiccup

そこで必要になるのが**Hiccup**です。
Hiccupは、HTML を Vector として記述し、それをライブラリ側が解釈してHTMLに変換します。例えば

```clj
(html [:p])
```

このコードは

```html
<p></p>
```

に変換されます。ちなみに、Clojure の Vector は 32分木をビット演算で走査する 形で実装されているため、かなり高速に動作します。これで HTMLを楽に記述する方法 は確保しました。しかし、まだ足りません。

### lib-noir

* リダイレクト
* セッション / クッキー
* 入力値の検査(validation)
* 内容のキャッシュ
* パスワードのハッシュ化

などなど、compojureだけではどうにもならない機能は少なくないです。これらを解決してくれるのが **lib-noir** です。詳しい使い方は後述します。
ページ表示周りのライブラリは揃いました。次に必要になるのはデータベースへのアクセスです。

### java.jdbc

データベースにアクセスするには java.jdbc ライブラリを使います。 Java用のドライバがあるデータベース としては MySQL が有名ですが、このライブラリは他にも SQLite, HSQLDB, PostgreSQL, Apache Dirby などのデータベース用のドライバもセットでインストールします。

さて、これでMVC周りは揃いました。以下に記載するライブラリは Twitterとの連携に必要 になるものです。

### clj-http

HTTPクライアント用のライブラリです。HTTPアクセスする際に必要になるパラメータを、マップ、ベクタ、キーワードなどのClojureの便利なリーダマクロとデータ構造を使って書けるようになっています。

### clj-oauth

Twitter といえば避けて通れないのが OAuth ですね。このライブラリは OAuth認証に必要な機能 を一通り揃えてくれています。

なお、Clojure の Twitterクライアントライブラリ といえば twitter-api がありますが、もうちょっと便利なものが欲しいので自作します。

### lein-ring

これは ClojureでServlet APIを動かすためのラッパとClojureからServlet APIにアクセスするための規約を定義したClojure用のサーバライブラリであるring を コマンドから操作する ための leiningen プラグイン です。
`lein ring server <port>` で現在開発中のウェブアプリケーションの実態をブラウザで確認でき(ブラウザが起動する)、 `lein ring server-headless <port>` でブラウザの起動なしにウェブアプリケーションを動かすことができます。
`lein ring war` や `lein ring uberwar` で、 Tomcat用のウェブアプリケーションのアーカイブ を作成できます。

### lein-auto

ソースコードの変更を即座に反映し、その反映されたコードに対して自動的にテストを実行してくれます。ウェブアプリケーション開発の必須アイテムです。プラグインをインストールして `lein auto test` と打つだけでOKです。

### codox

ソースコード中の :doc メタを参照して、ドキュメントを生成してくれます。プラグインをインストールした後 `lein doc` と打てば、その時点で記述されている :doc メタからドキュメントが生成されます。詳しい使い方は[weavejester/codox](https://github.com/weavejester/codox)を参照してください。

## プロジェクトの作成

必要なライブラリは一通り確保したので、次はいよいよプロジェクトディレクトリを作成し、プログラミングを始めましょう。プロジェクトのスケルトンを作るのは簡単です。シェル上で `lein new compojure <project-name>` と打てば compojure用のプロジェクトスケルトン が作成されます。

### project.clj の記述

プロジェクトディレクトリの中にある project.clj に 依存ライブラリ や 必要なleiningenプラグイン の情報を記述していきます。

```clj
(defproject <project-name> "0.1.0-SNAPSHOT"
  :descriptions "プロジェクトの概要"
  :url "ソースコードを保管しているリポジトリなどのURL"
  :license
    {:name "このプロジェクトが従属するライセンスの名前"
     :url "ライセンスの条文が置かれているサイトのURL"}
  :dependencies
    [
      [org.clojure/clojure "1.6.0"]
      [org.clojure/data.json "0.2.5"]
      [org.clojure/java.jdbc "0.3.6"]
      [compojure "1.3.1"]
      [hiccup "1.0.5"]
      [lib-noir "0.9.5"]
      [clj-http "1.0.1"]
      [clj-oauth "1.5.1"]
    ]
  :plugins
    [
      [lein-ring "0.8.13"]
      [lein-auto "0.1.1"]
      [codox "0.8.10"]
    ]
  :ring
    {:handler <project-name>.core.handler/app}
  :profiles
    {:dev
      {:dependencies
        [
          [javax.servlet/servlet-api "2.5"]
          [ring/ring-mock "0.2.5"]
        ]
      }
    })
```

project.clj を記述したら、同じディレクトリ上で `lein deps` を実行し、依存しているライブラリを一通りインストールしましょう。おや、まだ見慣れない記述がありますね。
`:profiles` は、 そのパッケージをインストールして利用する際には必要としない ものの、そのプロジェクトの 特定の場面 、 ユーザレベルで常に依存するライブラリやプラグイン の情報を記述する箇所です。 `:dev` プロファイルは開発時のみ必要となる(そのプロジェクトの中で `lein deps` を実行した時にだけインストールされる)ライブラリやプラグインの情報を記述します。
この場合は、開発している間のみ Servlet API と ring-mock が必要だということになります。

ring-mock は compojureやringで作成したウェブアプリケーションのコード のための テストライブラリ です。clojure.jarにあるAPIだけでは、ウェブアプリケーションのAPIやURLルートに対してテストをするのは困難なので、その部分を埋めてくれます(clj-http でも同じような事は可能ですが、 テスト目的であればlocalhost以外へのアクセスは不要 なので、今回は ring-mock を使います)。

## URLのルーティング

まずは、どのURLが呼ばれた時に、どの関数を返すかを決めましょう。ここで覚えておいて欲しいことがあります。ルートと割り当てる関数を決めたら…

1. まずは、「そのルートを通して取得・保存するデータを格納しておくための場所」と「そこで保存しているデータベースのレコードなどの構造」を明確かつ可能な限り正確に決める。
2. 次に、「そのルートへのアクセスを試みて、正しいパラメータを渡したら正しいデータが返ってくるか、間違ったデータを渡したらちゃんとエラーを返すようになっているか」を確かめるための **テストコード** を書く。
3. そして、肝心の関数を書き、それが終わったら、テストを走らせる。

これを1関数1ルート単位でちゃんとやること。ページプラグインのこととか、具体的なHTML出力について気にするのは、実装予定の関数が正確なデータを返すことを一通り確かめた後、例えば結合テストの手前あたりで十分です。ページのレイアウトが崩れるとかいう現象は単にタグやスタイルシートの使い方が間違っているか、スタイルシートを読み込めていないだけなので、実際のセキュアやデータの正確性にはほとんど影響しません。では、作業を始めましょう。
プロジェクトディレクトリの src/&lt;project-name&gt;/core/handler.clj を見てみましょう。

```clj
(ns <project-name>.core.handler
  (:require
    [compojure.core :refer :all]
    [compojure.route :as route]
    [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Hello world")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))
```

初めはこんな風になってます。
トップレベルで束縛されているシンボル app が :ring {:handler ...} と対応しています。 `wrap-defaults` は、プロキシ、セッションやクッキーなどといった、ウェブサービスに必要な諸々の設定をライブラリ側でしてくれます。 `site-defaults` は、その設定情報を束縛したシンボル(マップ)です。 project.clj の :ring {:handler ...} には、root `/` をコントロールしているシンボル(ハンドラ)を渡します。
ring-defaultsパッケージのAPI は、元々は compojure.handler にありました。現在では ring側のミドルウェア としてリファクタリングされています。

## テストコードで入出力されるデータを整理する。

手始めに、 URLから渡されてきたid と Twitterのstatus_id とを対応させてみましょう。まずはテストコードを作成します。ここでチェックすべきポイントは以下の2つです:

* 正しいデータを渡した時に HTTP Status 200 が返ってくるか。
* 200で返ってきた時に受け取ったJSONフォーマット が tweets.jsonの形 に従っているか

ここでいう「正しいデータを渡した」とは以下の4つの条件に合致したものを指します:

* Twitter側が指示した通りにOAuthパラメータを渡している。
* リクエストメソッドが合っている。
* 必須パラメータを渡している。
* 渡したIDと同じステータスIDのツイートが返される。

```clj
; <project-name>/test/<projet-name>/core/handler_test.clj

(ns <project-name>.core.handler-test
  (:require
    [clojure.test :refer :all]
    [ring.mock.request :as mock]
    [<project-name>.core.handler :refer :all]
    [data.json :as json]
    [oauth.client :as oauth]
    [clj-http.client :as http]))

; /statuses/xx にアクセスしたら、ちゃんとそのIDのツイートが返ってくるか。
; ステータスコードが200 で ちゃんと:bodyが持ってるツイートのIDが渡したIDと一致しているか を見る。
; ステータスコードが返らないなんてことはないだろうけど、型を合わせるために、ステータスコードが見つからない時は -1 を返すようにさせる。show.json からの id 取得も同様。
(deftest test-status
  (are [valid-status requested-status] (= valid-status requested-status)
    200 (get
          (app (mock/request :get "/<screen_name>/status/xx"))
          :status
          -1)
    ; JSON を マップに変換する のを忘れずに。
    xx  (->
          (app (mock/request :get "/<screen_name>/status/xx"))
          (get :body "")
          (json/read-str :key-fn keyword)
          (get :id -1))))
```

```clj
; <project-name>/src/<project-name>/core/handler.clj

(ns <project-name>.core.handler
  (:require
    [data.json :as json]
    [oauth.client :as oauth]
    [clj-http.client :as http]))

(def ^:private consumer-key #_hoge)
(def ^:private consumer-secret #_fuga)

(def ^:private consumer
  (oauth/make-consumer
    <consumer-key>
    <consumer-secret>
    "https://api.twitter.com/oauth/request_token"
    "https://api.twitter.com/oauth/access_token"
    "https://api.twitter.com/oauth/authorize"
    :hmac-sha1))

(def ^:private request-token
  (oauth/request-token
    consumer
    <callback-uri>))

; アプリケーション認証画面(/oauth/authorize?oauth_token=xxxx...) のURLを生成する。
; 「Twitterアカウントでログイン」ボタンなどのリダイレクト先に必要
(def ^:private user-approval-uri
  (oauth/user-approval-uri
    consumer
  (:oauth_token request-token)))

(def ^:private access-token-response
  (oauth/access-token
    consumer
    request-token
    <verifier>))

(defn- credentials [url params]
  (oauth/credentials
    consumer
    (:oauth_token access-token-response)
    (:oauth_token_secret access-token-response)
    :GET
    (str url params)))

(defn- status-show [^long id]
  (let
    [
      url    "http://api.twitter.com/1.1/statuses/show/"
      ; OAuthライブラリの側にクエリ文字列のためのWrapperはない。
      params {
               :headers
                 {"authorization" (authorization-header
                                    (credentials
                                      url
                                      (str id ".json")))}
               :query-params
                 {:id id}
             }
    ]
    (http/get url params)))

(defroutes app-routes
  (context "/:screen_name" [^String screen_name]
    (GET "/" [] #_"こっち側のサーバ から 最新のデータを取得 する処理")
    (GET "/status/:id" [^long id] (status-show id))))

(def app
  (wrap-defaults app-routes site-defaults))
```

これで終わり？いやいや、**もうちょっとだけ続く**んじゃ。このサービスの目的はあくまでもTwitter連携(+ 少々の補完)。ということは、まず Twitterで認証を済ませ 、 Twitter側の/oauth/authorize APIからoauth_tokenを受け取る 必要があります。

となると、テストコードにも追記が必要になりますね。以下の3点を確かめる必要があります:

* https://api.twitter.com/oauth/authorize から リダイレクト されてくるか
* URLクエリ に oauth_token はあるか
* oauth_token がリダイレクトの前後で変化していないか(滅多に起こらないと思うけどね)

```clj
; <project-name>/test/<projet-name>/core/handler_test.clj

(ns <project-name>.core.handler-test
  (:require
    [clojure.test :refer :all]
    [ring.mock.request :as mock]
    [<project-name>.core.handler :refer :all]
    [data.json :as json]
    [oauth.client :as oauth]
    [clj-http.client :as http]))

; /statuses/xx にアクセスしたら、ちゃんとそのIDのツイートが返ってくるか。
; ステータスコードが200 で ちゃんと:bodyが持ってるツイートのIDが渡したIDと一致しているか を見る。
; ステータスコードが返らないなんてことはないだろうけど、型を合わせるために、ステータスコードが見つからない時は -1 を返すようにさせる。show.json からの id 取得も同様。
(deftest test-status
  (are [valid-status requested-status] (= valid-status requested-status)
    200 (get
          (app (mock/request :get "/<screen_name>/status/xx"))
          :status
          -1)
    xx  (->
          (app (mock/request :get "/<screen_name>/status/xx"))
          (get :body "")
          (json/read-str :key-fn keyword)
          (get :id -1))))

; コードがネストする場合、
;
; (are [valid-param requested-param] (= valid-param requested-param)
;   valid-param
;     requested-param
;   valid-param
;     requested-param
;   ...)
;
; という風に書くことにする。
(deftest test-authorize
  (are [valid-param requested-param] (= valid-param requested-param)
    ; Twitter側の認証画面にリダイレクトできているか
    (str "https://api.twitter.com/oauth/authorize?oauth_token=" <token>)
      (get-in
        (app (mock/request :get "/auth/twitter"))
        [:headers "location"]
        "")

    ; Twitterからリダイレクト された時、 oauth_token と oauth_verifier は正しく保存されているか。(Twitterの /oauth/authorize での認証が終わってこちら側にリダイレクトしてきた時に、パラメータに渡されてきた oauth_token と oauth_verifier が クッキーに残っているか を確認する)
    true
      (let
        [
          request (mock/request :get "/auth/twitter" {:oauth_token <token> :oauth_verifier <verifier>})
          request (mock/header request "referer" (str "https://api.twitter.com/oauth/authorize?=" <token>))
          requested (app request)
        ]
        (and
          from-twitter?
          (not-any?
            false?
            [
              (get-in requested [:headers "Set-Cookie" "oauth_token"] false)
              (get-in requested [:headers "Set-Cookie" "oauth_verifier"] false)
            ])))))
```

で、後は routes に /auth/twitter を書き加えれば、最初の1歩はOKです。続きはまた次の機会に。

```clj
; <project-name>/src/<project-name>/core/handler.clj

(ns <project-name>.core.handler
  (:require
    [data.json :as json]
    [oauth.client :as oauth]
    [clj-http.client :as http])))

(def ^:private consumer-key #_hoge)
(def ^:private consumer-secret #_fuga)

(def ^:private consumer
  (oauth/make-consumer
    <consumer-key>
    <consumer-secret>
    "https://api.twitter.com/oauth/request_token"
    "https://api.twitter.com/oauth/access_token"
    "https://api.twitter.com/oauth/authorize"
    :hmac-sha1))

(def ^:private request-token
  (oauth/request-token
    consumer
    <callback-uri>))

; アプリケーション認証画面(/oauth/authorize?oauth_token=xxxx...) のURLを生成する。
; 「Twitterアカウントでログイン」ボタンなどのリダイレクト先に必要
(def ^:private user-approval-uri
  (oauth/user-approval-uri
    consumer
  (:oauth_token request-token)))

(def ^:private access-token-response
  (oauth/access-token
    consumer
    request-token
    <verifier>))

(defn- credentials [url params]
  (oauth/credentials
    consumer
    (:oauth_token access-token-response)
    (:oauth_token_secret access-token-response)
    :GET
    (str url params)))

(defn- status-show [^long id]
  (let
    [
      url    "http://api.twitter.com/1.1/statuses/show/"
      ; OAuthライブラリの側にクエリ文字列のためのWrapperはない。
      params {
               :headers
                 {"authorization" (authorization-header
                                    (credentials
                                      url
                                      (str id ".json")))}
               :query-params
                 {:id id}
             }
    ]
    (http/get url params)))

(defroutes app-routes
  ; Twitter認証(今回の記事では省略)
  (GET "/auth/twitter" [] get-auth)

  (context "/:screen_name" [^String screen_name]
    (GET "/" [] #_"こっち側のサーバ から 最新のデータを取得 する処理")
    (GET "/status/:id" [^long id] (status-show id))))

(def app
  (wrap-defaults app-routes site-defaults))
```

では、よいお年を！