ClojureでシンプルなTodo APIを作る
こんにちは、sudukiです。
今記事では、ClojureでシンプルなAPIを作成しようと思います。
知識の整理と、ハンズオンで利用するサーバー側実装を兼ねてます。
この記事を読むことで、
- Duct・Integrantの初歩の初歩的な概念
- DuctによるシンプルなCRUD APIの作成方法
が分かる予定です。(なお、Ductのドキュメントとほぼ一緒の内容です)
逆に、
- Clojureの文法・実装
- 自分でhandlerを実装する APIの作成方法
これらは対象外です。いずれ整理しようかとも思っていますが、予定は未定です。
環境
clojureのdocker imageから、VSCode Remote Containersを利用して作成しました。
詳細は省略させていただきます。
筆者の技術的背景
Clojureに関しては、Clojurescript koansを一周した程度で、どの書き方がどのリテラルかすら曖昧な状態です。
普段はhaskellでTUIアプリケーションを作ったり、purescript・たまにelmでクライアントサイドを実装したりしています。
興味関心としてはIdrisやAgdaなど、静的型付けの関数型言語の比重がとても高いです。業務ではreactを利用しています。
なので、関数型に関しては多少の慣れがある状態です。
ライブラリ選定
今回は作成することでClojureの感覚を掴みたいと思っています。なので、Ductを利用することとしました。
-
Luminus
- プロジェクトテンプレート
- プログラムというよりテンプレート→Clojureらしさを学びづらいのでは?
-
Pedestal
- セキュリティ等が out of the box
- pythonにおけるDjangoみたいな位置づけに見える
- 少し重厚そうなので見送り
-
Duct
- マイクロフレームワーク
- pythonにおけるflaskぐらいの軽さっぽい
- なんとなく責務が小さそうなのでキャッチアップが楽かもしれない
知識概要
Ductはデータ駆動webアプリフレームワーク
Ductは、key-value形式のデータからwebアプリを生成するフレームワークです。
もう少し正確に言うと、Integrantというフレームワークを拡張したものになります。
Ductはとてもシンプルなフレームワークなので、routing library等は他で用意することになります。
(ただ、公式がmoduleとして提供しているものもあるので、それを利用すれば特に煩わされることは無いでしょう。)
Integrantは実行時に依存関係を解決するフレームワーク
Integrantは、Ductの根幹をなすフレームワークです。
Integrantはkey-value形式のmapを参照して、実行時に依存関係を解決してくれます。
(def config {:somekey/a {:somevalue "値"}
:somekey/b [依存する :somekey/a]})
ちなみに、Ductは当初Componentというライブラリを利用していた。依存性を解決するためにmapではなく関数を利用していたため、改良版としてIntegrantが生まれたそうだ
マクロより関数、関数よりデータというプラクティス
Ataraxy - Routing library
Ataraxyとは、Ductが公式で対応しているルーティングライブラリです。
mapからルーティングのhandler1を算出してくれます。
似たようなルーティングライブラリとしてCompojureというものが存在します。Compojureは関数の連鎖でhandler1を生成します。
(Ataraxy ⇔ Compojure) は(Integrant ⇔ Component) と似たような関係性です。
実装
今回はとてもシンプルなCRUDのみのAPIを実装する予定です。
Integrantの*Composite key*という機能と、duct/handler.sqlというライブラリを利用して実装します。
手順
環境構築は済んでいるものとします。
lein new duct todoapi +api +sqlite +ataraxy
- ductのプロジェクトを生成
lein duct setup
- duct用のローカル設定ファイルを生成
lein repl
- replで確認しながら開発していく
(dev)
- devというnamespaceを読み込み。深く理解していないが、ファイルからデータを読み込んでいそう
(go)
- アプリケーション開始、integrantの機能
(reset)
- これで実行中のアプリケーションをリセット、integrantの機能
system config
以降、依存関係を解決するためにIntegrantに渡すmapのことを便宜上System configと呼ぶことにします。実態はただのmapです。
プロジェクトを生成した場合、resources/config.ednがSystem configの中核となります。
:duct.profile/baseというkeyword2に値を紐付けます。なお、keywordの字面から察せられる通りDuct側が定義したものです。(その他にdev等のconfigも存在しますが、今回は立ち入りません。)
composite-key
composite keyは、Integrant(composite keys)で実装を継承(OOPとは無関係)するための機能です。
System configのkeyとしてvector3・valueとして引数を渡すことで、他の実装を継承することができます。
例)
(def config {
[:継承元/foo :継承先/bar] {:継承元が必要としている "引数たちの"
:シンプルな "map"}
})
database migration
データベースを利用する際、migrationのライブラリが必要になります。Ductが公式で対応しているのはRagtimeというライブラリです。
今回はシンプルなCRUD APIのため、詳細には立ち入らずに*Composite key*によって実装を継承しようと思います。
:upにデータベースの準備、:downにデータベースのリセットをするSQL文を持つ、mapを渡すのみです。
{:duct.profile/base
{:duct.core/project-ns todoapi
;;; ...
:duct.migrator/ragtime
{:migrations [#ig/ref :todoapi.migration/create-todos]}
[:duct.migrator.ragtime/sql :todoapi.migration/create-todos]
{:up ["CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT)"]
:down ["DROP TABLE todos"]}
;;; ...
}
;;; ...
}
routing
Ductが提供するAtaraxyのモジュールを利用します。
このモジュールは、:routes
というkeyに紐付けられたmapでhandler1とrequestを対応付けることを想定しています。
それっぽく読むことができますが、かんたんに説明すると
requestをkeyとして持ち、
handlerへのkey(Integrantの概念としての)をvalueとして持ちます。
詳しくはこちら
{:duct.profile/base
{:duct.core/project-ns todoapi
;;; ...
:duct.router/ataraxy
{:routes {[:get "/"] [:todoapi.handler.todos/index]
[:get "/todos"] [:todoapi.handler.todos/list]
[:post "/todos" {{:keys [title]} :body-params}]
[:todoapi.handler.todos/create title]
[:get "/todos/" id] [:todoapi.handler.todos/find ^int id]
[:put "/todos/" id {{:keys [title]} :body-params}]
[:todoapi.handler.todos/update ^int id title]
[:delete "/todos/" id] [:todoapi.handler.todos/destroy ^int id]}}
;;; ...
}
;;; ...
}
handlers
今回実装するhandler1は2種類あります。
-
requestを受け取って、静的情報を返す
-
requestを受け取って、sql文を実行しレスポンスを返す
-
静的情報
こちらは:duct.handler.static/okを継承するのみです。
responseとして :bodyを入れてあげるだけのシンプルな実装です。 -
SQL
公式でduct/handler.sqlというライブラリが公開されているので、これをComposite keyにより継承して実装します。
duct/handler.sqlで公開されているkeyは、
- :duct.handler.sql/query
- :duct.handler.sql/query-one
- :duct.handler.sql/insert
- :duct.handler.sql/execute
の4つです。valueとしてrequestの受け取り方や、sql等を渡しています(全て理解してませんが、雰囲気で使えそうです)
公式のdocumentationもわかりやすいです:thumsup:
{:duct.profile/base
{:duct.core/project-ns todoapi
;;; ...
;;; 静的情報
[:duct.handler.static/ok :todoapi.handler.todos/index]
{:body {:todos "/todos"}}
;;; SQL
[:duct.handler.sql/query :todoapi.handler.todos/list]
{:sql ["SELECT * FROM todos"]
:hrefs {:href "/todos/{id}"}}
[:duct.handler.sql/insert :todoapi.handler.todos/create]
{:request {[_ title] :ataraxy/result}
:sql ["INSERT INTO todos (title) VALUES (?)" title]
:location "/todos/{last_insert_rowid}"}
[:duct.handler.sql/query-one :todoapi.handler.todos/find]
{:request {[_ id] :ataraxy/result}
:sql ["SELECT * FROM todos WHERE id = ?" id]
:hrefs {:href "/todos/{id}"}}
[:duct.handler.sql/execute :todoapi.handler.todos/update]
{:request {[_ id title] :ataraxy/result}
:sql ["UPDATE todos SET title = ? WHERE id = ?" title id]}
[:duct.handler.sql/execute :todoapi.handler.todos/destroy]
{:request {[_ id] :ataraxy/result}
:sql ["DELETE FROM todos WHERE id = ?" id]}
;;; ...
}
;;; ...
}
結果
get http://localhost:3000
get http://localhost:3000/todos
get http://localhost:3000/1
post http://localhost:3000/todos title="すごいタスク"
put http://localhost:3000/todos/4 title="すごくないタスク"
delete http://localhost:3000/4
感想
Clojureはその柔軟な言語構造のおかげで、様々な思想のライブラリがあることに衝撃を受けました。
シンプルに実装できるが、その反対に実装に上手いこと制約をかけるのが難しそうだなという率直な感想…。
どういう風に対応しているんだろう?
mailiやclojure.specなどを利用するみたいなので、気が向いたら記事を書いてみたいです。