シリーズバックナンバー
- 何も考えずに Emacs を使って Clojure の Luminus webframework を使う
- Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き
- Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き
- Luminus で GCE 上の API を叩いてみる
Luminus 使いこなしたい
Clojure の WebFramework には Duct などパワフルでフレキシブルなものがたくさんありますが、手っ取り早く Web サーバをうねうねするには Luminus が早いかなというお気持ちな今日この頃です。
さて私事ではございますが、先日大学の授業の一貫で、6時間程度でFlask を用いて機械学習のAPIを作って、それらを使って Web でうまいこと叩けるようにするというタスクがございました。所謂チキンレースという奴です。
その際にWeb側を作成する際に用いたのが、この Luminus というわけです。
結論から言いますと、ブラウザからAPIを確認することが出来る Swagger というツール越しにこれを試す事が出来ました。
ところが開発をしていく上で一々ブラウザに移動して打ち込むのは面倒くさいので、何とか repl 上で動かしたいなと思い、雑にコードを書きました。
環境
OS: Manjaro Linux
Build tool for Clojure: leiningen
Editor: Emacs (ciderを repl として利用)
WebFramework: Luminus (lein new luminus demo-app +reitit +aleph +swagger +re-frame +auth +oauth
)
Luminus のオプションが がもりもりしていますが、今回で重要なのは、reitit
の部分だけです。
コード ({project-name}/src/clj/{project-name}/handler.clj に追記)
;; (mount/defstate app
;; ...
;; )
;;;;;;;;;;;;;;;;;;;;
(defn- api-confirm
([]
(with-open [rdr (clojure.java.io/reader (:body (app {:request-method :get, :uri "/api/ping"})))]
(-> rdr
line-seq
first
(clojure.data.json/read-str :key-fn keyword))))
([^String method ^String uri]
(assert (contains? #{"get" "post"} method))
(with-open [rdr (clojure.java.io/reader (:body (app {:request-method (keyword method), :uri uri})))]
(-> rdr
line-seq
first
(clojure.data.json/read-str :key-fn keyword))))
([^String method ^String uri ^clojure.lang.PersistentArrayMap params]
(assert (contains? #{"get" "post"} method))
(with-open [rdr (clojure.java.io/reader
(:body
(app (merge
{:request-method (keyword method)
:uri uri}
params))))]
(-> rdr
line-seq
first
(clojure.data.json/read-str :key-fn keyword)))))
;; (api-confirm) => {:message "pong"}
;; (api-confirm "hogehoge" "/foo") => assertion error
;; (api-confirm "get" "/api/ping") => {:message "pong"}
;; see. {project-name}/src/clj/routes/services
;; (api-confirm "get" "/api/math/plus" {:query-params {:x 2 :y 1}}) => {:total 3}
;; see. {project-name}/src/clj/routes/services
;; (api-confirm "post" "/api/math/plus" {:body-params {:x 2 :y 1}}) => {:total 3}
;; see. {project-name}/src/clj/routes/services
簡単にコードを説明すると、defn-
で内部関数の(defn
だと外部に公開される)3つの引数パターンを持つ関数 api-confirm
を作りました。
付録として引数パターンを上から見ていきます。
追記
Flask を用いて機械学習APIを書いた話は、~~~この部分が卒業研究に含まれているため~~~ もう少しするまで公開できません。
セキュリティ周りの話になるとちょっと面倒なので、気力があれば後に別の記事を書きます。
付録
一つ目
一つ目は、何も引数を取らないパターンです。
(app {:request-method :get, :uri "/api/ping"})
まず app
に対して、/api/ping
にパラメータなしで get
リクエストを投げます。
(:body (app {:request-method :get, :uri "/api/ping"}))
するとマップ形式で返ってくるので、body 要素にアクセスします(ちなみに status 要素にアクセスすると 200 が返ってきます)。
(clojure.java.io/reader (:body (app {:request-method :get, :uri "/api/ping"}))))
この中身はちょっと不思議な型(java.io.ByteArrayInputStream)になっているので、これを clojure.java.io/reader
関数で処理します。
これを rdr
として関数内のローカル変数とします(ここやや語弊あり。python で言う with open ... as rdr
です)。
(-> rdr
line-seq
first
(clojure.data.json/read-str :key-fn keyword))
;; =>
;; (clojure.data.json/reader-str
;; (first
;; (line-seq rdr)
;; )
;; :key-fn keyword)
ここからは Threading-macro を使っています。これは第一引数(今回で言う rdr
)を第二引数以降に連なる関数に対して 第一引数として 順番に適用していきます。
まず、rdr
を line-seq
関数を通して、java.io.BufferedReader型から String 型のリストに変換します。
今回はリストの一番目が重要になるので、これを first
関数で持ってきます。これは json がテキストになっています({"message":"pong"})。
jsonのテキストを clojure で扱いやすいようにするには clojure.data.json/read-str
関数が良いでしょう。
引数 :key-fn
として keyword
を指定していますが、これは json の 名前の部分 (message) を clojure の keyword にするということです。
;; (api-confirm) => {:message "pong"}
Clojure や lisp 系の他言語では一般に関数の最後の部分が返り値になるので、以上の処理を行った結果が返ってきます。
二つ目
二つ目は引数として method 名と uri を取ります。
(assert (contains? #{"get" "post"} method))
(clojure.java.io/reader (:body (app {:request-method (keyword method), :uri uri})))
一つ目との差異はここだけです。1行目では今回扱う method 名は get と post だけだったので、これ以外の(集合 #{"get" "post"} 以外の) method を扱わないように assertion をかけました。2行目は method 名と uri を引数のそれで埋めています。
;; (api-confirm "get" "/api/ping") => {:message "pong"}
これで一つ目と同じ出力を得ることができます。
三つ目
三つ目は引数として method 名と uri そしてパラメータを取ります。パラメータは merge
関数で method 名、uri を含んでいるマップに統合されます。
パラメータは、query ならば :query-params
、 body ならば :body-params
の value に書きます。
例として、query x, y を受け取る get method の /api/plus
は
;; (api-confirm "get" "/api/math/plus" {:query-params {:x 2 :y 1}}) => {:total 3}
となり body x, y を受け取る post method の /api/plud
は
;; (api-confirm "post" "/api/math/plus" {:body-params {:x 2 :y 1}}) => {:total 3}
となります。