RingとCompojureにキャッチアップ
Clojureキャッチアップのために
Clojureでwebアプリを作るためのキャッチアップとして。
こちらのスクラップの派生として作業している。
Compojureの動きを確かめる
とりあえずこのページを参考にして、まず動くものを作っていってみる。
参考にと言っても、まずそれぞれのライブラリを動かしてみて、それから上記の記事を読んでいこうと思うので、今回は感触を掴むためのものを作るとこまで。
Compojureの動きを確かめる
簡単にCompojureの動きを確かめてみる。
まずはプロジェクトへの追加。
project.clj
(defproject sample-compojure "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.11.1"]
[compojure "1.7.1"]
[ring/ring-jetty-adapter "1.9.6"]]
:main ^:skip-aot sample-compojure.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
以上の依存関係を解決するためにコマンドを入力。
lein deps
core.clj
(ns sample-compojure.core
(:require
[compojure.core :refer [defroutes GET]]
[ring.adapter.jetty :refer [run-jetty]]))
(defroutes app-routes
(GET "/" [] "Hello, Compojure!"))
(run-jetty app-routes {:port 3000})
アクセスして確かめる。
まずサーバーの起動。
lein run
それからアクセス。
http://localhost:3000
パラメータの動きも確かめる。
ついでにPOSTも追加してみる。
(ns sample-compojure.core
(:require
[compojure.core :refer [defroutes GET POST]]
[ring.adapter.jetty :refer [run-jetty]]))
(defroutes app-routes
(GET "/" [] "Hello, Compojure!")
(GET "/hello/:name" [name] (str "Hello, " name "!"))
(POST "/submit" [data] (str "Received data: " data)))
(run-jetty app-routes {:port 3000})
POSTの方はエラーが起きた。
ブラウザからアクセスだけしているのでむべなるかな。
ターミナルからデータを送ってみる。
$ curl -X POST http://localhost:3000/submit -d "Bob"
Received data:
うまく言ってないので、コードを修正。
(ns sample-compojure.core
(:require
[compojure.core :refer [defroutes GET POST]]
[ring.middleware.params :refer [wrap-params]]
[ring.adapter.jetty :refer [run-jetty]]))
(defroutes app-routes
(GET "/" [] "Hello, Compojure!")
(GET "/hello/:name" [name] (str "Hello, " name "!"))
(POST "/submit" [data] (str "Received data: " data)))
;; wrap-paramsを追加
(def app
(wrap-params app-routes))
(run-jetty app {:port 3000})
curl -X POST http://localhost:3000/submit -d "data=Bob"
Received data: Bob%
成功した。
Compojureの感想
今のところ、以下のような機能を持っているように思える。
- ルーティング
- GETやPOSTの機能
ringの機能を借りているものとしては、
- サーバーを立てる部分をrun-jettyから借りている
といったところだろうか。
予想
GETやPOSTを持っているので、PUTやDELETEももちろん持っているだろう。
それぞれの書き方を把握しておいたほうがいいかもしれない。
Ringの動きを確かめる
今度はRingの動きを確かめていく。
プロジェクト作成
テスト用のプロジェクト作成から。
lein new app sample-ring
依存関係追加
つぎに依存関係を追加する。
project.clj
(defproject sample-ring "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.0"]
[ring/ring-jetty-adapter "1.9.0"]]
:main ^:skip-aot sample-ring.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
lein deps
コードを書く
動作確認のサンプルコードを書く。
(ns sample-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response]]))
(defn handler [request]
(response "Hello, World!"))
(defn -main []
(run-jetty handler {:port 3000 :join? false}))
サーバーを起動して、http://localhost:3000
にアクセスして動作確認。
ちょっと改造
これだと面白くないので、ちょっと改造してみた。
(ns sample-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response content-type]]))
(defn handler-walter [request]
(-> (response "621。お前に意味を与えてやる。")
(content-type "text/plain; charset=UTF-8")))
(defn -main []
(run-jetty handler-walter {:port 3000 :join? false}))
文字化け対策にcharsetの指定を入れている。
RingとCompojureの組み合わせ
Ringのサンプルで試したコードにCompojureを付け加えてみる。
まず依存関係にcompojureを追加する。
project.clj
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.0"]
[ring/ring-jetty-adapter "1.9.0"]
[compojure "1.7.1"]]
さて、本体のコードにハンドラを追加し、compojureを使ってルーティングする。
(ns sample-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response content-type]]
[compojure.core :refer [routes GET]]
[compojure.route :as route]))
(defn handler-hello [request]
(response "Hello, Clojure!"))
(defn handler-walter [name]
(-> (response (str name "。" "お前に意味を与えてやる。仕事の時間だ。"))
(content-type "text/plain; charset=UTF-8")))
(defn handler-not-found [request]
(-> (response "404 Not Found")
(content-type "text/plain; charset=UTF-8")
(assoc :status 404)))
(def app
(routes
(GET "/" [] handler-hello)
(GET "/walter/:name" [name] (handler-walter name))
(route/not-found handler-not-found)))
(defn -main []
(run-jetty app {:port 3000 :join? false}))
lein run
してそれぞれのパスにアクセスしてみる。
lein run
http://localhost:3000
http://localhost:3000/walter/404
404ページ。
JSONのレスポンス
JSONでのレスポンスコードを書いてみる。
JSONを扱えるようにするため、依存関係にcheshireを追加する。
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.0"]
[ring/ring-jetty-adapter "1.9.0"]
[cheshire "5.13.0"]
[compojure "1.7.1"]]
Clojarsというサイトがあるらしく、サイト上でdependencies用のコードをコピーできるようなので便利。
画像にあるCopyボタンを押して、project.clj
に追加すればいい。
(ns sample-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response content-type]]
[compojure.core :refer [routes GET]]
[compojure.route :as route]
[cheshire.core :as json]))
(defn handler-hello [request]
(response "Hello, Clojure!"))
(defn handler-walter [name]
(-> (response (str name "。" "お前に意味を与えてやる。仕事の時間だ。"))
(content-type "text/plain; charset=UTF-8")))
;; json用のレスポンスハンドラを追加
(defn handler-json [request]
(-> (response (json/generate-string {:message "これはJSONレスポンスです" :status "success"}))
(content-type "application/json; charset=UTF-8")))
(defn handler-not-found [request]
(-> (response "404 Not Found")
(content-type "text/plain; charset=UTF-8")
(assoc :status 404)))
(def app
(routes
(GET "/" [] handler-hello)
(GET "/walter/:name" [name] (handler-walter name))
;; JSONレスポンス用のルートパスを追加
(GET "/json" [] handler-json)
(route/not-found handler-not-found)))
(defn -main []
(run-jetty app {:port 3000 :join? false}))
JSONのリクエスト
今度はリクエスト側のコードを書く。
まず依存関係を追加。
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.0"]
[ring/ring-jetty-adapter "1.9.0"]
[ring/ring-json "0.5.1"]
[cheshire "5.13.0"]
[compojure "1.7.1"]]
コードを書いていく。
(ns sample-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.util.response :refer [response content-type]]
[compojure.core :refer [routes GET POST]]
[compojure.route :as route]
[cheshire.core :as json]
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]))
(def charset "charset=UTF-8")
(defn handler-hello [request]
(response "Hello, Clojure!"))
(defn handler-walter [name]
(-> (response (str name "。" "お前に意味を与えてやる。仕事の時間だ。"))
(content-type (str "text/plain;" charset))))
(defn handler-json [request]
(-> (response (json/generate-string {:message "これはJSONレスポンスです" :status "success"}))
(content-type (str "application/json;" charset))))
(defn handler-json-request [request]
(let [data (:body request)
name (:name data)]
(-> (response (json/generate-string {:message (str "Hello, " (or name "Unknown") "!") :status "success"}))
(content-type (str "application/json;" charset)))))
(defn handler-not-found [request]
(-> (response "404 Not Found")
(content-type (str "text/plain;" charset))
(assoc :status 404)))
(def app
(-> (routes
(GET "/" [] handler-hello)
(GET "/walter/:name" [name] (handler-walter name))
(GET "/json" [] handler-json)
(POST "/json-request" [] handler-json-request)
(route/not-found handler-not-found))
(wrap-json-body {:keywords? true})
(wrap-json-response)))
(defn -main []
(run-jetty app {:port 3000 :join? false}))
あとはサーバーを起動してリクエストの確認。
lein run
別ターミナルと立ち上げて、curlを使ってJSONリクエストを送ってみる。
$ curl -X POST http://localhost:3000/json-request -H "Content-Type: application/json" -d '{"name": "Handler Walter"}'
{"message":"Hello, Handler Walter!","status":"success"}%
テストコード
各エンドポイントをテストするコードを書く。
依存関係を追加。
ring/ring-mock
とclj-http
。
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.0"]
[ring/ring-jetty-adapter "1.9.0"]
[ring/ring-json "0.5.1"]
[ring/ring-mock "0.4.0"]
[cheshire "5.13.0"]
[clj-http "3.13.0"]
[compojure "1.7.1"]]
さて、テストコードを書いていく。
src/test/project/core_test.clj
(ns sample-ring.core-test
(:require [clojure.test :refer :all]
[ring.adapter.jetty :refer [run-jetty]]
[ring.mock.request :as mock]
[sample-ring.core :refer :all]
[clj-http.client :as client]
[byte-streams :as bs]
[cheshire.core :as json]))
(defonce server (atom nil))
(defn start-server []
(reset! server (run-jetty app {:port 3000 :join? false})))
(defn stop-server []
(when @server
(.stop @server)
(reset! server nil)))
(use-fixtures :once
(fn [tests]
(start-server)
(try
(tests)
(finally
(stop-server)))))
(deftest test-hendler-hello
(testing "GET /"
(let [response (client/get "http://localhost:3000/")
body (:body response)]
(is (= 200 (:status response)))
(is (= "Hello, Clojure!" body)))))
(deftest test-handler-walter
(testing "GET /walter/:name"
(let [response (app (mock/request :get "/walter/621"))]
(is (= 200 (:status response)))
(is (= "text/plain;charset=UTF-8" (get-in response [:headers "Content-Type"])))
(is (= "621。お前に意味を与えてやる。仕事の時間だ。" (bs/to-string (:body response)))))))
(deftest test-handler-not-found
(testing "GET /unknown"
(let [response (app (mock/request :get "/unknown"))]
(is (= 404 (:status response)))
(is (= "404 Not Found" (bs/to-string (:body response)))))))
(deftest test-handler-json
(testing "GET /json"
(let [response (app (mock/request :get "/json"))
body (json/parse-string (bs/to-string (:body response)) true)]
(is (= 200 (:status response)))
(is (= "application/json;charset=UTF-8" (get-in response [:headers "Content-Type"])))
(is (= {:message "これはJSONレスポンスです" :status "success"} body)))))
(deftest test-handler-json-request
(testing "POST /json-request"
(let [data {:name "621"}
response (app (-> (mock/request :post "/json-request")
(mock/json-body data)))
body (json/parse-string (bs/to-string (:body response)) true)]
(is (= 200 (:status response)))
(is (= "application/json;charset=UTF-8" (get-in response [:headers "Content-Type"])))
(is (= {:message "Hello, 621!" :status "success"} body))))
(testing "POST /json-request without name"
(let [response (app (mock/request :post "/json-request"))
body (json/parse-string (bs/to-string (:body response)) true)]
(is (= 200 (:status response)))
(is (= "application/json;charset=UTF-8" (get-in response [:headers "Content-Type"])))
(is (= {:message "Hello, Unknown!" :status "success"} body)))))
ハマったポイント
書いていく中で、cheshireでのJSONパースにちょっとハマったので、記事化した。