Immutant+Liberator+ReagentでWebサービスを作る素振り

Webサービスを作る例。

いろんな人が散々やっているところではあるが、とりあえず練習で作っておけばいざというときに役に立つという、素振り練習的な意味で。

全体はこちら: https://github.com/t2ru/webexample

# サーバ側

## 使用ライブラリ

* Immutant (JBoss)
* Liberator
* Compojure
* org.clojure/
  - data.json
  - java.jdbc

## 設計ポリシ

* ImmutantでJBossのインフラをなるべく活用する。
  - Webサーバ
  - トランザクション制御
* DBアクセスはclojure.java.jdbcを直で使う。
  - 後でチューニングしやすいように、SQLの抽象化は使わない。
  - XAトランザクションで囲う。(ただし、XAの信じすぎ良くない。)
  - サンプルのDBはH2（ファイルDB）を使う。商用でJBossのコンテナに載せるときに、datasourceに変えてコネクションプールを使えば良い。
* WebサービスはLiberatorを使ってREST準拠にする。
  - URLパスでのルーティングにはCompojureを使う。
* Compojureのハンドラ内に全部書いてしまう。
  - API内で使っているSQLを全部把握できるため、非効率なSQL発行は一目瞭然。
  - 認可情報やSQLなどを全て一覧できるようにすることで、目視のセキュリティチェックをやりやすくなる。
* SQLインジェクションやXSSの攻撃をされない設計


## コード例

```clojure:src/webexample/core.clj
(defroutes task-service
  ;; コンテナ系リソース
  (GET "/task" {:keys [db]}
    (resource
      :allowed-methods [:get]
      :available-media-types ["application/json"]
      :handle-ok (list-tasks db)))

  (POST "/task" {:keys [db body]}
    (let [data (try (as-json body :key-fn keyword :eof-error? false)
                    (catch Exception e nil))
          new-id (or (:newid (first (next-task-id db))) 0)]
      (resource
        :allowed-methods [:post]
        :available-media-types ["application/json"]
        :malformed? (nil? (:title data))
        :handle-malformed (pr-str data)
        :post! (fn [_] (new-task! db new-id (:title data)))
        :handle-created {:id new-id})))

  ;; エレメント系リソース
  (GET "/task/:id" [id :as {:keys [db]}]
    (resource
      :allowed-methods [:get]
      :available-media-types ["application/json"]
      :handle-ok (fn [_] (first (get-task db)))))

  (PUT "/task/:id" [id :as {:keys [db body]}]
    (let [data (try (as-json body :key-fn keyword :eof-error? false)
                    (catch Exception e nil))]
      (resource
        :allowed-methods [:put]
        :available-media-types ["application/json"]
        :malformed? (nil? (:title data))
        :put! (fn [_] (update-task! db (:title data) id))
        :new? false)))

  (DELETE "/task/:id" [id :as {:keys [db]}]
    (resource
      :allowed-methods [:delete]
      :available-media-types ["application/json"]
      :delete! (fn [_] (delete-task! db id)))))
```

SQLはyesqlを使って外出し。

```sql:resource/data/task.sql
-- name: create-task-table!
CREATE TABLE IF NOT EXISTS task (
	id INTEGER PRIMARY KEY,
	title VARCHAR(255))

-- name: list-tasks
SELECT id, title FROM task ORDER BY id DESC

-- name: next-task-id
SELECT MAX(id)+1 AS newid FROM task

-- name: new-task!
INSERT INTO task (id, title) VALUES (:id, :title)

-- name: get-task
SELECT id, title FROM task WHERE id = :id

-- name: update-task!
UPDATE task SET title = :title WHERE id = :id

-- name: delete-task!
DELETE FROM task WHERE id = :id
```

## 設計のこころ

重要なものを明示し、一覧できるようにすること。そうすることで、性能問題やセキュリティ問題の発生を防ぐ。テストやセキュリティチェックツールに頼らず[^1]、作る時にバグを排除できるようにする。 (Correctness by Construction)

[^1]: テストいらないということではなく、バグ発見をテストに頼るべきでないという意味。

# クライアント側

## 使用ライブラリ

* Reagent (react.js)
* cljs-ajax

## 設計ポリシ

* DOM生成部分とハンドラ部分に分けて整理する。
  - react.js (ClojureからはReagent) を使えば、細かいハンドラ部分をかなり省略できる。
* Webデザイナとの分業を視野に入れ、JavaScriptのDOM生成で全部作ってしまうのは避ける。(サンプルではやってない)
* レンダリングをなるべく一度で済ませるようにして、性能劣化を防ぐ。
  - Reagentになるべく任せて自分でやらないほうがいい気がする。
* JQuery UIみたいなUI要素ごとの部品化(これもやってない)


## コード例

aは、アプリケーション状態を保存するatom。

サーバへのCRUDとブラウザ内部の状態更新を行う。

```clojure:ハンドラ部(src-cljs/webexample/core.cljs)
(defn list-tasks [a]
  (GET "task"
       {:handler #(reset! a %)
        :error-handler #(js/alert %)}))

(defn add-task [a]
  (let [elem (dom/get-element "newtask")
        title (.-value elem)
        f (fn [v response] (cons {"id" (get response "id") "title" title} v))]
    (when-not (= title "")
      (set! (.-value elem) "")
      (POST "task"
            {:params {:title title}
             :format :json
             :handler #(swap! a f %)
             :error-handler #(js/alert %)})))
  false)

(defn update-task [a id]
  (let [elem (dom/get-element "newtitle")
        new-title (.-value elem)
        f (fn [v] (map (fn [{xid "id" xtitle "title" :as t}]
                         (if (= id xid) {"id" id "title" new-title} t)) v))]
    (when-not (= new-title "")
      (PUT (str "task/" id)
           {:params {:title new-title}
            :format :json
            :handler #(swap! a f)
            :error-handler #(js/alert %)})))
  false)

(defn delete-task [a id]
  (let [f (fn [v] (filter #(not= id (get % "id")) v))]
    (DELETE (str "task/" id)
            {:format :json
             :handler #(swap! a f)
             :error-handler #(js/alert %)})))
```

```clojure:DOM生成部(src-cljs/webexample/core.cljs)
(defn task-app []
  (let [task-list (atom nil)
        editing (atom nil)]
    (list-tasks task-list)
    (fn []
      [:div
       [:input {:type :submit :value "Sync"
                :on-click (fn []
                             (reset! editing nil)
                             (list-tasks task-list))}]
       [:form {:on-submit #(add-task task-list)}
        [:input#newtask {:type :text}]
        [:input {:type :submit}]]
       (->> (for [task @task-list
                  :let [{id "id" title "title"} task]]
              [:li {:on-click #(reset! editing id)}
               id " "
               (if (= id @editing)
                 [:input#newtitle
                  {:type :text :defaultValue title
                   :on-blur
                   (fn []
                     (reset! editing nil)
                     (update-task task-list id))}]
                 [:span title " "
                  [:span {:on-click #(delete-task task-list id)} "[x]"]])])
            (cons :ul)
            vec)])))

(defn main-app []
  [:div
   [:h1 "list tasks"]
   [(task-app)]])

(defn ^:export run []
  (reagent/render-component [main-app] (.-body js/document)))
```


# まだやってないこと

* 認証
* 入力のバリデーション
* 一覧のページング
* 国際化
* 見た目のデザイン
* ネット接続が不安定な環境への対応
* core.asyncが使えるかもしれない。主にクライアント側で。
* etc...

気が向いたら更新するかも。