LoginSignup
2
0

More than 5 years have passed since last update.

Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き

Last updated at Posted at 2019-02-10

シリーズバックナンバー

  1. 何も考えずに Emacs を使って Clojure の Luminus webframework を使う
  2. Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き
  3. Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き
  4. 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)を第二引数以降に連なる関数に対して 第一引数として 順番に適用していきます。

まず、rdrline-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}

となります。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0