Clojureでコメント投稿用のAPIを作った
Pedestal が簡単そうなので、これを使ってコメント投稿とコメント取得のみのシンプルなAPIを作ってみたいと思います。
方針
- APIの出力はJSON形式
- DBはとりあえずSQLite
- クエリはHugSQLで扱う
- コメントはページごとに扱えるようにする
- 承認前のコメントは表示させない
- APIをGETしたりPOSTしたりする側はjQueryでいいか
実装
まずは leiningen で Pedestal プロジェクトを作る。
lein new pedestal-service comment-api-server
project.cljの:dependenciesに下記を追加。
[com.layerware/hugsql "0.5.1"]
[org.xerial/sqlite-jdbc "3.28.0"]
[org.clojure/java.jdbc "0.7.10"]
同じく :resource-paths に sql を追加。
sql ディレクトリも作成しておく。
:resource-paths ["config", "resources", "sql"]
sql/comments.sql を作ってクエリを書く。
-- :name create-comments-table
-- :command :execute
-- :result :raw
-- :doc Create comments table
create table comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_id TEXT,
name TEXT,
mail TEXT,
comment TEXT,
ip TEXT,
approved INTEGER DEFAULT 0,
created_at DATETIME DEFAULT (datetime('now', 'localtime'))
)
-- A :result value of :n below will return affected rows:
-- :name insert-comment :! :n
-- :doc Insert a single comment returning affected row count
insert into comments (page_id, name, mail, comment, ip)
values (:page_id, :name, :mail, :comment, :ip)
-- :name comments-by-page :? :*
-- :doc Comments
select *
from comments
where page_id = :page_id and approved = 1
order by id desc
limit 100 offset 0
-- :name not-approved-comments :? :*
-- :doc Comments
select *
from comments
where approved = 0
DB 接続周りを書く。
ひとまず固定で書いてるけどそのうち外に出す想定で。
data/db.clj を作成する。
(ns data.db
(:require [clojure.java.jdbc :as jdbc]))
(def db-spec
{:subprotocol "sqlite",
:subname "path/to/comment.sqlite"})
次は HugSQL でクエリを読み込む辺り。
data/comments.clj を作成。
(ns data.comments
(:require [hugsql.core :as hugsql]))
(hugsql/def-db-fns "comments.sql")
次はプロジェクト作成時に作られている service.clj を変更。
comment-api-server/service.clj
:require に追加。
[io.pedestal.log :as log]
[data.comments :as comments]
[data.db :as db]
[clojure.walk :as walk]
バリデーションを作っておく。
(defn validate-email-pattern
[mail]
(let [pattern #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"]
(and (string? mail) (re-matches pattern mail))))
(defn validate-email
[mail]
(if (validate-email-pattern mail)
nil
"mail pattern mismatch"))
(defn validate-name
[name]
(if (and (string? name) (> (count name) 0))
nil
"empty name"))
(defn validate-comment
[comment]
(if (and (string? comment) (> (count comment) 0))
nil
"empty comment"))
(defn new-comment-validator
[input]
(let [name-error (validate-name (:name input))
email-error (validate-email (:mail input))
comment-error (validate-comment (:comment input))]
(if (or name-error email-error comment-error)
[name-error email-error comment-error]
nil)))
DB アクセス周りの関数は分離しておく。
(defn exists-db
[path]
(.exists (clojure.java.io/as-file path)))
(defn create-db-if-not-exists
[]
(if (not (exists-db (:subname db/db-spec)))
(comments/create-comments-table db/db-spec)))
(defn db-comments
[page-id]
(create-db-if-not-exists)
(let [cs (seq (comments/comments-by-page db/db-spec {:page_id page-id}))]
(if cs cs '())))
(defn db-new-comment
[page-id ip params]
(create-db-if-not-exists)
(let [p (assoc (walk/keywordize-keys params) :page_id page-id :ip ip)
validation-errors (new-comment-validator p)]
(if validation-errors
{:success 0, :messages (filter #(not (nil? %)) validation-errors)}
(if (comments/insert-comment db/db-spec p)
{:success 1}
{:success 0, :messages ["insert failure"]}))))
ルートに定義する関数を実装する。
(defn get-comments
[request]
(let [page-id (:page-id (:path-params request))
comments (db-comments page-id)]
(ring-resp/response comments)))
(defn add-comment
[request]
(let [page-id (:page-id (:path-params request))
result (db-new-comment page-id (:remote-addr request) (:params request))]
(ring-resp/response {:success result})))
ルートを定義する。
(def common-interceptors [(body-params/body-params) http/json-body])
(def routes #{["/comment/:page-id" :get (conj common-interceptors `get-comments) :constraints {:page-id #"[a-zA-Z0-9\-_/]+"}]
["/comment/:page-id" :post (conj common-interceptors `add-comment) :constraints {:page-id #"[a-zA-Z0-9\-_/]+"}]})
後は def service 中の ::http/allowed-origins を無効かして設定する。
::http/allowed-origins {:creds true :allowed-origins ["https://www.sysbe.net"]}
動作テスト。 現状ではオリジンをハードコーディングしてるので、テストは run-dev で。
lein run-dev
run-dev の定義は server.clj にあって、その中で ::server/allowed-origins を上書きしているようです。
これでAPI自体はだいたい完成。
コメント承認周りのフォームは作ってないけど、とりあえずはクエリー直接打てばいいよね。
その後
現在の版では、コンフィグファイルを読み込んでDB接続情報、::http/allowed-origins を定義するようにしています。
config.edn の例。
{:db-spec {:subprotocol "sqlite", :subname "target/sample_db.sqlite"}
:allowed-origins {:creds true :allowed-origins ["https://www.example.com"]}
:admin {:allowed-ip ["127.0.0.1"]}}
また、ちょっとしたバリデーションも追加しています。
起動方法は自動化したほうがいいんだろうけど、とりあえず手動で。
nohup java -Dfile.encoding=utf-8 -jar comment-api-server-0.0.2-SNAPSHOT-standalone.jar config.edn &
Origin周りでトラブったりしてましたが、それに関しては次の記事で。