Closed8

RingとCompojureにキャッチアップ

kip2kip2

Compojureの動きを確かめる

とりあえずこのページを参考にして、まず動くものを作っていってみる。
https://ayato-p.github.io/clojure-beginner/intro_web_development/index.html

参考にと言っても、まずそれぞれのライブラリを動かしてみて、それから上記の記事を読んでいこうと思うので、今回は感触を掴むためのものを作るとこまで。

Compojureの動きを確かめる

簡単にCompojureの動きを確かめてみる。

https://github.com/weavejester/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%

成功した。

kip2kip2

Compojureの感想

今のところ、以下のような機能を持っているように思える。

  • ルーティング
  • GETやPOSTの機能

ringの機能を借りているものとしては、

  • サーバーを立てる部分をrun-jettyから借りている

といったところだろうか。

予想

GETやPOSTを持っているので、PUTやDELETEももちろん持っているだろう。
それぞれの書き方を把握しておいたほうがいいかもしれない。

kip2kip2

Ringの動きを確かめる

今度はRingの動きを確かめていく。

https://github.com/ring-clojure/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の指定を入れている。

kip2kip2

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ページ。

kip2kip2

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"]]

https://github.com/dakrone/cheshire?tab=readme-ov-file

Clojarsというサイトがあるらしく、サイト上でdependencies用のコードをコピーできるようなので便利。
https://clojars.org/cheshire

画像にある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}))

kip2kip2

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"}%
kip2kip2

テストコード

各エンドポイントをテストするコードを書く。

依存関係を追加。
ring/ring-mockclj-http
https://clojars.org/ring/ring-mock
https://clojars.org/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パースにちょっとハマったので、記事化した。

https://zenn.dev/kip2/articles/cheshire-parse-string

このスクラップは1日前にクローズされました