ClojureのWebFramework Luminusのチュートリアル

以下の公式ドキュメントを和訳しました。
[Clojure Luminus - Your first application](http://www.luminusweb.net/docs/guestbook.md)

この和訳は以下のバージョンのテンプレートを前提としています。
https://clojars.org/luminus/lein-template/versions/2.9.8.76

テンプレートのバージョンに関してはayato_pさんの以下の記事が参考になります。
[陳腐化しない Clojure の情報を書くために気をつけるべきこと/あるいは情報が陳腐化していることに気付くために知っておきたいこと](http://qiita.com/ayato_p/items/5358cff8897783bba6d2)

LuminusはClojureのさまざまなライブラリの集合体です。
このチュートリアルで個別のライブラリの使い方もある程度理解することができるでしょう。

# ゲストブックアプリ
このチュートリアルではLuminusを使ったシンプルなゲストブックアプリを構築する方法を説明する。
ゲストブックアプリはメッセージを投稿することと他のユーザーが投稿したメッセージのリストを表示することができる。
このアプリケーションはHTMLテンプレーティング、データベースアクセス、プロジェクト設計の基本を実演することになる。

もし、あなたがまだ好みのClojure用のエディタを持っていないとしたら、[LightTable](http://www.lighttable.com/)を使用してこのチュートリアルを行うことをおすすめする。

## JDKのインストール
ClojureはJVM上で動作するので、JDKがインストールされている必要がある。
もし、あなたの環境にJDKがまだインストールされていない場合、OpenJDKをおすすめする。
それは[ここ](http://www.azul.com/downloads/zulu/)からダウンロードできる。
ちなみにLuminusはデフォルト設定でJDK 8を必要とすることを記しておく。

## Leiningenのインストール
Luminusを動かすためには[Leiningen](http://leiningen.org/)をインストールする必要がある。
Leiningenは以下のような簡単な手順でインストールできる。

1. スクリプトをダウンロードする
1. スクリプトに実行権限を付与する(eg: chmod +x lein)
1. パスの通ったディレクトリにそれを配置する(eg: ~/bin)
1. 「lein self-install」を実行し、インストールが完了するのを待つ

```bash
$ wget https://raw.github.com/technomancy/leiningen/stable/bin/lein
$ chmod +x lein
$ mv lein ~/bin
$ lein
```

## 新しいアプリケーションの作成
Leiningenのインストールができたら、アプリケーションの初期化のためにターミナルで以下のコマンドを実行する。

```bash
$ lein new luminus guestbook +h2
$ cd guestbook
```

上記のコマンドは[H2 embedded database](http://www.h2database.com/html/main.html)を使用する新しいテンプレートプロジェクトを作成する。

## Luminusアプリケーションの詳解
新しく作成したアプリケーションは以下のような構造をしている。

```
guestbook
|____.gitignore
|____Procfile
|____project.clj
|____profiles.clj
|____README.md
|____src
| |____guestbook
|   |____core.clj
|   |____handler.clj
|   |____layout.clj
|   |____middleware.clj
|   |____routes
|   | |____home.clj
|   |____db
|     |____core.clj
|     |____migrations.clj
|____env
| |____dev
| | |____clj
| |   |____guestbook
| |     |____config.clj
| |     |____dev_middleware.clj.clj
| |____prod
|   |____clj
|     |____guestbook
|       |____config.clj
|____test
| |____guestbook
|   |____test
|     |____handler.clj
|
|____resources
  |____templates
  | |____about.html
  | |____base.html
  | |____home.html
  | |____error.html
  |____public
  | |____css
  | | |____screen.css
  | |____img
  | |____js
  |____docs
  | |____docs.md
  |____migrations
  | |____20150718103127-add-users-table.down.sql
  | |____20150718103127-add-users-table.up.sql
  |____sql
   |____queries.sql
```

アプリケーションの最上位のフォルダに入っているファイルに関して見ていこう。

* `Procfile` - Herokuへのデプロイを支援するために使われる
* `README.md` - 伝統的にアプリケーションのドキュメントが書かれるファイル
* `project.clj` - Leiningenがプロジェクトの設定とライブラリの依存関係を管理するために使われる
* `profiles.clj` - コードのリポジトリに入れたくないローカル設定を行うために使われる
* `.gitignore` - ビルドによって生成されたファイル等のGitのバージョン管理から除外するファイルのリスト

## ソースディレクトリ
すべてのソースコードはsrcフォルダ以下に置かれる。
今回作成するアプリケーションはguestbookという名前なので、プロジェクトの最上位の名前空間はこのguestbookになる。
では、作成されたすべての名前空間に関して見ていこう。

### guestbook

* `core.clj` - サーバーの起動と終了に関するロジックを含む、このアプリケーションのエントリポイントである。
* `handler.clj` - アプリケーションの基本的なルーティングを定義する。これはアプリケーションのエントリポイントである。
* `layout.clj` - ページのコンテンツを描画するためのレイアウトヘルパーを記述するための名前空間である。
* `middleware.clj` - アプリケーションのカスタムミドルウェアを含む名前空間である。

### guestbook.db
`db`名前空間はアプリケーションのモデルを定義し、永続化層を処理するために使用される。

* `core.clj` - データベースとのやり取りのための関数を保持するために使用される
* `migrations.clj` - データベースマイグレーションを実行するために使用される

### guestbook.routes
`routes`名前空間はhomeとaboutページのルーティング定義とコントローラが配置される場所である。
あなたが認証のような複数のルーティング定義や特定のワークフローを追加する場合は、ここにそのための名前空間を作成する必要がある。

* `home.clj` - アプリケーションのhomeページとaboutページを定義する名前空間

## Envディレクトリ
これは環境依存コードのソースを含むフォルダである。dev設定は開発中に使用され、prod設定は本番環境用にコンパイルされるときに使用される。

`config.clj` - 通常は開発用設定を含む
`dev_middleware.clj` - 本番ではコンパイルされるべきでない開発時に使用するミドルウェアを含む

## テストディレクトリ
ここはアプリケーションのテストを配置する場所であり、いくつかのサンプルテストがすでに定義されている。

## リソースディレクトリ
これはアプリケーションのすべての静的なリソースを配置する場所である。
resourcesディレクトリ下のpublicディレクトリに配置したファイルはアプリケーションサーバーによってクライアントにそのまま配信される。
いくつかのCSSリソースがすでに作成されている。

### HTMLテンプレート
`templates`ディレクトリはページ表示のための[Selmer](https://github.com/yogthos/Selmer)テンプレートのために準備されている。

`about.html` - aboutページ
`base.html` - サイトのベースレイアウト
`home.html` - homeページ

### SQLクエリ
SQLクエリは`resources/sql`フォルダの中にある。

`queries.sql` - SQLクエリとそれらに結びつく関数を定義する。

### マイグレーションディレクトリ
Luminusは[Migratus](https://github.com/yogthos/migratus)でマイグレーション管理を行っている。
マイグレーションはupとdownのSQLファイルで管理される。

これらのファイルは慣例的に日付を使ってバージョン管理され、順番に適用される。
```
20150718103127-add-users-table.up.sql - テーブル作成のためのマイグレーションファイル
20150718103127-add-users-table.down.sql - テーブル削除のためのマイグレーションファイル
```

## プロジェクトファイル
上で記述したように、すべての依存関係は`project.clj`ファイルを更新することによって管理される。私たちの作成したアプリケーションのプロジェクトファイルはルートフォルダに存在し、以下のような内容となっている。

```clj
(defproject guestbook "0.1.0-SNAPSHOT"

  :description "FIXME: write description"
  :url "http://example.com/FIXME"

:dependencies [[org.clojure/clojure "1.7.0"]
               [selmer "0.8.7"]
               [com.taoensso/timbre "4.0.2"]
               [com.taoensso/tower "3.0.2"]
               [markdown-clj "0.9.67"]
               [environ "1.0.0"]
               [compojure "1.4.0"]
               [ring-webjars "0.1.1"]
               [ring/ring-defaults "0.1.5"]
               [ring-ttl-session "0.1.1"]
               [ring "1.4.0"
                :exclusions [ring/ring-jetty-adapter]]
               [metosin/ring-middleware-format "0.6.0"]
               [metosin/ring-http-response "0.6.3"]
               [bouncer "0.3.3"]
               [prone "0.8.2"]
               [org.clojure/tools.nrepl "0.2.10"]
               [org.webjars/bootstrap "3.3.5"]
               [org.webjars/jquery "2.1.4"]
               [migratus "0.8.2"]
               [conman "0.1.1"]
               [to-jdbc-uri "0.2.0"]
               [com.h2database/h2 "1.4.187"]
               [org.immutant/web "2.0.2"]]

  :min-lein-version "2.0.0"
  :uberjar-name "guestbook.jar"
  :jvm-opts ["-server"]

  :main guestbook.core
  :migratus {:store :database}

  :plugins [[lein-environ "1.0.0"]
            [lein-ancient "0.6.5"]
            [migratus-lein "0.1.5"]]
  :profiles
  {:uberjar {:omit-source true
             :env {:production true}
             :aot :all}
   :dev           [:project/dev :profiles/dev]
   :test          [:project/test :profiles/test]
   :project/dev  {:dependencies [[ring/ring-mock "0.2.0"]
                                 [ring/ring-devel "1.4.0"]
                                 [pjstadig/humane-test-output "0.7.0"]
                                 [mvxcvi/puget "0.8.1"]]


                  :repl-options {:init-ns guestbook.core}
                  :injections [(require 'pjstadig.humane-test-output)
                               (pjstadig.humane-test-output/activate!)]
                  ;;when :nrepl-port is set the application starts the nREPL server on load
                  :env {:dev        true
                        :port       3000
                        :nrepl-port 7000}}
   :project/test {:env {:test       true
                        :port       3001
                        :nrepl-port 7001}}
   :profiles/dev {}
   :profiles/test {}})
```

`project.clj`はアプリケーションについてそれぞれ異なった観点から記述した、キーと値のペアを含んだ単なるClojureのリストであることが分かるだろう。

たいていの場合、新しいライブラリをプロジェクトに追加することが多い。これらのライブラリは特に`:dependencies`のベクトルを使う。新しいライブラリをプロジェクト内で使うには、私たちはただ依存関係をここに追加するだけでいい。

`:plugins`ベクトル内の要素は環境変数の読み込み（via. Environ plugin）などの機能性の追加を提供するために使われる。

`:profiles`は、開発あるいは本番環境のビルドに向けてプロジェクトを初期化するために使用する。
これは環境ごとに異なるmap形式の設定となっている。

このプロジェクトは`:dev`と`:test`の複合プロファイルとして設定していることを記しておく。
これらのプロファイルは`:project/dev`と`:project/test`からの変数だけでなく、`profiles.clj`中の`:project/dev`と`:project/test`からの変数も含んでいる。

後者には共有のコードリポジトリにチェックインしないローカル環境変数を含むべきである。

`project.clj`ビルドファイルの構築に関するより詳細な事項に関しては公式の`Leiningen`のドキュメントを参照してください。

## データベースの作成
まず、アプリケーションのモデルを作るために`migrations`フォルダ中に配置されている`<date>-add-users-table.up.sql`を開こう。
ファイルは以下のような記述になっている。

```sql
CREATE TABLE users
(id VARCHAR(20) PRIMARY KEY,
 first_name VARCHAR(30),
 last_name VARCHAR(30),
 email VARCHAR(30),
 admin BOOLEAN,
 last_login TIME,
 is_active BOOLEAN,
 pass VARCHAR(100));
```

以下のように`users`テーブルを私たちのアプリケーションにより適切な形に書き換えよう。

```sql
CREATE TABLE guestbook
(id INTEGER PRIMARY KEY AUTO_INCREMENT,
 name VARCHAR(30),
 message VARCHAR(200),
 timestamp TIMESTAMP);
```

`guestbook`テーブルはコメントした人の名前、メッセージの内容、タイムスタンプなどのメッセージを記述するすべてのフィールドを保存する。
ファイルを保存してプロジェクトのルートディレクトリで以下のコマンドを実行しよう。

```bash
$ lein run migrate
```

もし、すべてがうまく行ったなら、これでデータベースが初期化されるはずだ。

## データベースへのアクセス

次に`src/guestbook/db/core.clj`ファイルを見てみよう。
ここにはデータベースへの接続設定がすでにあることが分かる。

```clj
(ns guestbook.db.core
  (:require
    [clojure.java.jdbc :as jdbc]
    [yesql.core :refer [defqueries]]
    [taoensso.timbre :as timbre]
    [environ.core :refer [env]])
  (:import java.sql.BatchUpdateException))

(def conn
  {:classname   "org.h2.Driver"
   :connection-uri (:database-url env)
   :make-pool?     true
   :naming         {:keys   clojure.string/lower-case
                    :fields clojure.string/upper-case}})

(defqueries "sql/queries.sql" {:connection conn})
```

データベースの接続設定は実行時の`:database-url`環境変数から読み込まれる。
この変数は開発環境の場合は`profiles.clj`から取り込まれ、本番環境の場合は以下のように環境変数に設定されている必要がある。

```bash
$ export DATABASE_URL="jdbc:h2:./guestbook.db"
```

H2 embedded databaseを使用するため、URLの中でプロジェクトを起動したディレクトリからの相対パスで指定したファイルにデータは保存される。

データベースのクエリにマッピングされる関数は`defqueries`が呼び出された時に生成される。
その`defqueries`は`sql/queries.sql`ファイルを参照していることが分かるだろう。
このファイルは`resources`の中にあるので、ファイルを開いて中身を見てみよう。

```sql
-- name: create-user!
-- creates a new user record
INSERT INTO users
(id, first_name, last_name, email, pass)
VALUES (:id, :first_name, :last_name, :email, :pass)

-- name: update-user!
-- update an existing user record
UPDATE users
SET first_name = :first_name, last_name = :last_name, email = :email
WHERE id = :id

-- name: get-user
-- retrieve a user given the id.
SELECT * FROM users
WHERE id = :id
```

各関数は`-- name:[関数名]`という形式のコメントを使用することで定義されていることが分かるだろう。
関数名を定義した次の行のコメントは関数のドキュメントになっており、さらに次の行からSQLを記述するような形式になっている。
SQLで使用するパラメータは`:[パラメータ名]`という表記になっている。
実際にクエリの内容を以下のように書き換えてみよう。

```sql
-- name:save-message!
-- creates a new message
INSERT INTO guestbook
(name, message, timestamp)
VALUES (:name, :message, :timestamp)

-- name:get-messages
-- selects all available messages
SELECT * from guestbook
```

最後にこの名前空間は`run`というヘルパー関数を提供してくれる。この関数は`conn`というアトムからデータベースへの接続を探してクエリ関数を実行する。

## アプリケーションを起動する
以下のようにして開発モードでアプリケーションを起動することができる。

```shell-session
>lein run
00:49:54.865 [main] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Slf4jLoggerProvider
15-Jul-19 00:49:55 Nyx INFO [guestbook.handler] - nREPL server started on port 7000
15-Jul-19 00:49:55 Nyx INFO [guestbook.handler] -
-=[guestbook started successfullyusing the development profile]=-
00:49:55.772 INFO  [org.projectodd.wunderboss.web.Web] (main) Registered web context /
15-Jul-19 00:49:55 Nyx INFO [guestbook.core] - server started on port: 3000
```

一度サーバーを起動したら、ブラウザで`http://localhost:3000`を開くことができ、アプリケーションが動作していることが分かるはずだ。起動時に以下のようなパラメータを指定するか、`PORT`という環境変数を設定することでサーバーを別のポートで起動させることができる。

```shell-session
lein run 8000
```

表示されたページが私たちにデータベースを初期化するためのマイグレーションを実行することを即してることに注意してほしい。しかし、私たちはすでにマイグレーションを以前行っているため、もう一度行う必要はない。

## ページの作成と入力フォームの取り扱い
ルーティング定義は`guestbook.routes.home`名前空間に定義されている。そのファイルを開き、データベースから取り出したメッセージを表示するためのロジックを追加しよう。
まずはじめに`db`、[Bouncer](https://github.com/leonardoborges/bouncer)、[ring.util.response](http://ring-clojure.github.io/ring/ring.util.response.html)の名前空間への参照を一緒に追加する必要がある。

```clj
(ns guestbook.routes.home
  (:require
    ...
    [guestbook.db.core :as db]
    [bouncer.core :as b]
    [bouncer.validators :as v]
    [ring.util.response :refer [redirect]]))
```

次にフォームのパラメータをバリデートするための関数を作る。

```clj
(defn validate-message [params]
  (first
    (b/validate
      params
      :name v/required
      :message [v/required [v/min-count 10]])))
```

この関数は`:name`と`:message`のキーに対応する値が私たちが定めたルール従っているかチェックするためにBouncerの`validate`関数を使用している。
具体的には、nameは必須で、messageは少なくとも10文字である必要があるようにしている。
Bouncerはmessageの場合のようにバリデータに複数のルールを渡すためにベクタ構文を使用している。

`min-count`を使用した場合のようにバリデータが追加のパラメータをとる場合、ベクタ構文が同様に使用される。その値は、暗黙的に第一引数としてバリデータに渡されます。

こうして、以下のようにメッセージをバリデートし、保存するための関数を追加した。

```clj
(defn save-message! [{:keys [params]}]
  (if-let [errors (validate-message params)]
    (-> (redirect "/")
        (assoc :flash (assoc params :errors errors)))
    (do
      (db/save-message!
       (assoc params :timestamp (java.util.Date.)))
      (redirect "/"))))
```

この関数はフォームのパラメータを含むリクエストからの`:params`キーを取得する。
`validate-message`関数がエラーを返した場合、`/`にリダイレクトし、エラーと一緒に提供されたパラメータを入れたレスポンスを`:flash`キーに関連付ける。
エラーを返さない場合、データベースにメッセージを保存し、リダイレクトする。

さらに`home-page`コントローラを以下のように変更する。

```clj
(defn home-page [{:keys [flash]}]
  (layout/render
   "home.html"
   (merge {:messages (db/get-messages)}
          (select-keys flash [:name :message :errors]))))
```

この関数はバリデートエラーなどの`:flash`セッションからのすべてのパラメータと一緒に現在保存されているメッセージをテンプレートに渡してhomeページの描画する。

ルーティングの設定としては`home-page`と`save-message!`の両方にリクエストを渡す必要がある。

```clj
(defroutes home-routes
  (GET "/" request (home-page request))
  (POST "/" request (save-message! request))
  (GET "/about" [] (about-page)))
```

`compojure.core`から`POST`を参照できるようにすることを忘れないようにしよう。

```clj
(ns guestbook.routes.home
  (:require ...
            [compojure.core :refer [defroutes GET POST]]
            ...))
```

コントローラが出来上がったので、`resources/templates`ディレクトリ内にある`home.html`テンプレートのファイルを開こう。
そのままの状態だと、`content`ブロック中の`content`変数の内容をただ表示するだけになっている。

```html
{% extends "base.html" %}
{% block content %}
  <div class="jumbotron">
    <h1>Welcome to guestbook</h1>
    <p>Time to start building your site!</p>
    <p><a class="btn btn-primary btn-lg" href="http://luminusweb.net">Learn more &raquo;</a></p>
  </div>

  <div class="row">
    <div class="span12">
    {{docs|markdown}}
    </div>
  </div>
{% endblock %}
```

メッセージを繰り返し処理するようにし、リスト中のそれぞれの値を表示するように`content`ブロックを変更しよう。

```html
{% extends "base.html" %}
{% block content %}
  <div class="jumbotron">
    <h1>Welcome to guestbook</h1>
    <p>Time to start building your site!</p>
    <p><a class="btn btn-primary btn-lg" href="http://luminusweb.net">Learn more &raquo;</a></p>
  </div>

  <div class="row">
    <div class="span12">
        <ul class="messages">
            {% for item in messages %}
            <li>
                <time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
                <p>{{item.message}}</p>
                <p> - {{item.name}}</p>
            </li>
            {% endfor %}
        </ul>
    </div>
</div>
{% endblock %}
```

上記のようにメッセージを繰り返し処理するためにforイテレータを使用した。それぞれのメッセージは`message`、`name`、　`timestamp`のキーを使用したマップになっているため、キーの名前でアクセスすることができる。タイムスタンプを人間が読むことのできる形式に変換する日付フィルターが使用できることも記しておこう。

最後にユーザーがメッセージの送信するためのフォームを作成する。ユーザーが送信したnameとmessageの値をテンプレートにはめ込み、それらに関連するすべてのエラーを描画する。
また、CSRF対策のためにフォーム内で`csrf-field`タグを使用している。

```html
<div class="row">
    <div class="span12">
        <form method="POST" action="/">
                {% csrf-field %}
                <p>
                    Name:
                    <input class="form-control"
                           type="text"
                           name="name"
                           value="{{name}}" />
                </p>
                {% if errors.name %}
                <div class="alert alert-danger">{{errors.name|join}}</div>
                {% endif %}
                <p>
                    Message:
                <textarea class="form-control"
                          rows="4"
                          cols="50"
                          name="message">{{message}}</textarea>
                </p>
                {% if errors.message %}
                <div class="alert alert-danger">{{errors.message|join}}</div>
                {% endif %}
                <input type="submit" class="btn btn-primary" value="comment" />
        </form>
    </div>
</div>
```

最終的な`home.html`テンプレートは以下のようになるだろう。

```html
{% extends "base.html" %}
{% block content %}
<div class="row">
    <div class="span12">
        <ul class="messages">
            {% for item in messages %}
            <li>
                <time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
                <p>{{item.message}}</p>
                <p> - {{item.name}}</p>
            </li>
            {% endfor %}
        </ul>
    </div>
</div>
<div class="row">
    <div class="span12">
        <form method="POST" action="/">
                {% csrf-field %}
                <p>
                    Name:
                    <input class="form-control"
                           type="text"
                           name="name"
                           value="{{name}}" />
                </p>
                {% if errors.name %}
                <div class="alert alert-danger">{{errors.name|join}}</div>
                {% endif %}
                <p>
                    Message:
                <textarea class="form-control"
                          rows="4"
                          cols="50"
                          name="message">{{message}}</textarea>
                </p>
                {% if errors.message %}
                <div class="alert alert-danger">{{errors.message|join}}</div>
                {% endif %}
                <input type="submit" class="btn btn-primary" value="comment" />
        </form>
    </div>
</div>
{% endblock %}
```

入力フォームをよりかっこよく整形するために`resources/public/css`フォルダに配置された`screen.css`ファイルを更新することができるようになった。

```css
body {
	height: 100%;
	padding-top: 70px;
	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #eaeaea;
	width: 550px;
	margin: 0 auto;
}

.messages {
  background: white;
  width: 520px;
}
ul {
	list-style: none;
}

ul.messages li {
	position: relative;
	font-size: 16px;
	padding: 5px;
	border-bottom: 1px dotted #ccc;
}

li:last-child {
	border-bottom: none;
}

li time {
	font-size: 12px;
	padding-bottom: 20px;
}

form, .error {
	width: 520px;
	padding: 30px;
	margin-bottom: 50px;
	position: relative;
  background: white;
}
```

これでブラウザのページをリロードすればゲストブックのページに対面することになるだろう。ゲストブックにコメントを追加し、それが正しく動作することを確認してみよう。

## アプリケーションのパッケージング
アプリケーションは以下のコマンドを実行することによってスタンドアローンでのデプロイ形式にすることができる。

```bash
$ lein uberjar
```

これは以下のようにして実行可能なjarを作成する。

```bash
$ java -jar target/guestbook.jar
```

このチュートリアルの完全なソースファイルは[ここ](https://github.com/yogthos/guestbook)で取得できる。より完全なサンプルを求める場合は[Github](https://github.com/yogthos/luminus)上にあるドキュメントページのソースファイルを読むといいだろう。