Clojure: RingでTodoアプリを作る - jsonを扱う

![advent_calendar_2024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/f4cd8ce0-6352-a1d8-4cae-f39dd44f4def.png)

[*Advent Calendar 2024 Day 15*](https://qiita.com/advent-calendar/2024/clojure)

昨日は`compojure`を使ってルーティングを設定しました。

今日はミドルウェアを使用してjsonを扱えるようにしていきます

---

`Ring` でJsonを扱う際には `ring-json` というライブラリが便利です

## ring-json

https://github.com/ring-clojure/ring-json

まずは依存を追加します

```clojure
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [...]
                 [ring/ring-json "0.5.1"]]
```

これで追加できました。

ring-jsonにはringでjsonを扱う際に便利なミドルウェアがいくつかありますが、
jsonレスポンスを返せるようにするためには `wrap-json-response` を使います

## wrap-json-response

公式ドキュメントにもあった例を実装してみます

```clojure
(ns todo.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.util.response :as res]
            [ring.middleware.json :refer [wrap-json-response]]))

(defn handler [req]
  (res/response {:foo "bar"}))

(def app
  (wrap-json-response handler))

(defn -main []
  (jetty/run-jetty app {:port 3000}))
```

これでサーバーを起動しリクエストを送ってみます

```bash
$ curl localhost:3000 
{"foo":"bar"}
```

`ring.util.response`にある `response` という関数で `response-body`を生成し、
`wrap-json-body`のミドルウェアでhandlerをwrapすることでjsonレスポンスを返せるようになりました。

昨日作成したルーティングにも組み込んでみます

```clojure
(ns todo.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.util.response :as res]
            [ring.middleware.json :refer [wrap-json-response]]
            [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]))

(defroutes handler'
  (POST "/v1/todos" [] (res/response {:message "作成しました"}))
  (GET "/v1/todos" [] (res/response {:message "Todoの一覧を返します"}))
  (GET "/v1/todos/:id" [id] (res/response {:message (str "Todoを返します(id:" id ")")}))
  (PUT "/v1/todos/:id" [id] (res/response {:message (str "更新しました(" id ")")}))
  (DELETE "/v1/todos/:id" [id] (res/response {:message (str "削除しました(id: " id ")")}))
  (route/not-found (res/response {:message "Not Found"})))

(def handler
  (-> handler'
      (wrap-json-response)))

(defn -main []
  (jetty/run-jetty handler {:port 3000}))

```

これでjsonレスポンスが返せるようになりました

```bash
$ curl localhost:3000/v1/todos
{"message":"Todoの一覧を返します"}
```

それぞれのメソッドの処理部分を関数に切り出し、Todoっぽくしてみます

```clojure
(ns todo.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.util.response :as res]
            [ring.middleware.json :refer [wrap-json-response]]
            [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]))

(defn- get-todo [id]
  (res/response {:id 1 :title "朝食を食べる" :completed false}))

(defn- get-todos [req]
  (res/response [{:id 1 :title "朝食を食べる" :completed false}
                 {:id 2 :title "昼食を食べる" :completed false}
                 {:id 3 :title "夕食を食べる" :completed false}]))

(defn- post-todos [req]
  (res/response {:id 1 :title "朝食を食べる" :completed false}))

(defn- put-todo [id]
  (res/response {:id 1 :title "朝食を食べる" :completed false}))

(defn- delete-todo [id]
  (res/response {:id 1 :title "朝食を食べる" :completed false}))

(defroutes handler'
  (POST "/v1/todos" req post-todos)
  (GET "/v1/todos" [] get-todos)
  (GET "/v1/todos/:id" [id] (get-todo id))
  (PUT "/v1/todos/:id" [id] (put-todo id))
  (DELETE "/v1/todos/:id" [id] (delete-todo id))
  (route/not-found (res/response {:message "Not Found"})))

(defn- wrap-response [handler]
  (fn [req]
    (-> (handler req)
        (res/response))))

(def handler
  (-> handler'
      (wrap-json-response)))

(defn -main []
  (jetty/run-jetty handler {:port 3000}))
```

```bash
$ curl localhost:3000/v1/todos                                                              
[{"id":1,"title":"朝食を食べる","completed":false},{"id":2,"title":"昼食を食べる","completed":false},{"id":3,"title":"夕食を食べる","completed":false}]
```

## wrap-json-body

次に、リクエストされるjsonも扱いやすくしていきます

`wrap-json-body`というミドルウェアを使うとそれを実現できます

ただ `res/response`の部分がとても冗長に感じるので、これもwrapするようなミドルウェアを作成します

```clojure
(def handler
  (-> handler'
      (wrap-json-response)
      (wrap-json-body {:keywords? true})))
```

このように ミドルウェアを足してみました

`POST: /v1/todos`に対して

```json
{
  "title": "テストタイトル",
  "completed": false
}
```
を送ると新しいTodoが作成されるようにしたいので、`post-todo`の関数を修正し、受け取れるようにしてみます

送られたjsonは、`request`の`body`にバインドされるため以下のようにして取り出すことにしました。

```clojure
(defn- post-todos [req]
  (let [{:keys [title completed]} (get-in req [:body])]
    (res/response {:id 1 :title title :completed completed})))
```

実際にリクエストを送ってみます

```bash
$ curl localhost:3000/v1/todos -X POST -d '{"title": "テストタイトル", "completed": false}' -H 'Content-Type: application/json'
{"id":1,"title":"テストタイトル","completed":false}
```

これでjsonを扱うことができるようになりました