Clojureで作ったAPIをマイクロサービスの海に隠す
とりあえず、一般的なDuctアプリケーションを作っておきます。これをベースに話を進めることにしましょう(現在のDuctのテンプレートは0.12.3が最新です)。 code:sh
$ lein new duct sample +api +ataraxy +postgres
ルーティング定義
最初はルーティング定義について話をしていきましょう。外部からAPIを叩くときにhttp://localhost/camelCaseとなっていてほしいのですが、アプリケーションの内部でルーティング定義を書くときにうっかりkebab-caseで書いてしまうかもしれません。うっかりkebab-caseで書いてしまっても大丈夫なようにしてみます。 まずはRingハンドラーを以下のように定義しておきます。
code:src/sample/handler/acme.clj
(ns sample.handler.acme
(defmethod ig/init-key ::camel-case _
(constantly (res/response {:lowerCamelCase false})))
次にconfig.ednです。どうやらうっかりさんが/camel-caseと書いてしまったようです。
code:resources/sample/config.edn
{
:duct.profile/base
{:duct.core/project-ns sample
:duct.router/ataraxy
{:routes
:sample.handler.acme/camel-case
{}}
;;………
}
この状態でAPIを起動してみると以下のようにリクエストをすることができます。
code:sh
$ curl 'localhost:3000/camel-case'
{"lower-camel-case":false}
このままだと他のAPIはURLのパスをkebab-caseで書かなくてはなりません。これだと約束を守れていないので軽くハックしていきます。以下のようにrouter.cljを書いてみます。 code:src/sample/router.clj
(ns sample.router
(derive ::camelize-router :duct.router/ataraxy)
(postwalk
(if (string? x)
(str/join "/"))
x))
routes))
(defmethod ig/init-key ::camelize-router {:keys routes :as options} (->> (update options :routes camelize-routes)
(ig/init-key :duct.router/ataraxy)))
もうひとつのポイントとしてはDuctで既に定義されている:duct.router/ataraxyをderiveしていることです。これによって、:duct.router/ataraxyを必要とするところに自動的にインジェクションされるコンポーネントを定義することができます。 code:project.clj
(defproject sample "0.1.0-SNAPSHOT"
;; ………
それからconfig.ednの:duct.router/ataraxyだったところを:sample.router/camelize-routerへと変更します。
code:resources/sample/config.edn
:sample.router/camelize-router
{:routes
code:sh
$ curl 'localhost:3000/camel-case'
{"error":"not-found"}
$ curl 'localhost:3000/camelCase'
{"lower-camel-case":false}
JSONレスポンス
さて、次はさっきからちらほら見えているJSONのレスポンスをlowerCamelCaseにしてみましょう。うっかりさんがsrc/sample/handler/acme.cljで(res/response {:lower-camel-case false})と書いてしまっているため、返ってくるJSONのキーがkebab-caseとなってしまっています。これをハンドラーを書き換えることなく、lowerCamelCaseなキーのJSONになるように修正していきます。 レスポンスに作用させるミドルウェアを定義します。ここでも先程と同様にinflectionsを利用することにします。 code:src/sample/middleware.clj
(ns sample.middleware
(defn wrap-camelize-response handler (-> (handler req)
(defmethod ig/init-key ::wrap-camelize-response _
wrap-camelize-response)
ざくっと書いたらconfig.ednを修正します。:duct.handler/rootの:middlewareに定義したミドルウェアを差込みます。Ductプロジェクトを作成するさいに+apiや+siteというオプションを渡していると最初からいくらかミドルウェアがインジェクションされているため、その設定とマージします。 code:resources/sample/config.edn
{:duct.profile/base
{:duct.core/project-ns sample
:duct.handler/root
{:middleware
:sample.middleware/wrap-camelize-response
{}
;;………
期待したようにミドルウェアを差込むことができたかは、REPLから以下のフォームを評価することで知ることができます。
code:clojure
clj꞉dev꞉> (pprint (:duct.handler/root config))
:middleware [#ig/ref :sample.middleware/wrap-camelize-response
#ig/ref :duct.middleware.web/not-found #ig/ref :duct.middleware.web/format #ig/ref :duct.middleware.web/defaults #ig/ref :duct.middleware.web/log-requests #ig/ref :duct.middleware.web/log-errors #ig/ref :duct.middleware.web/stacktrace]} nil
ここまで書けたらAPIを再起動して、リクエストを送ってみます。
code:sh
$ curl 'localhost:3000/camelCase'
{"lowerCamelCase":false}
JSONボディ
code:sh
$ curl -XPOST 'localhost:3000/postJson' -H 'content-type: application/json' -d '{"fooBarBaz": 42}'
例によってconfig.ednを修正して、ルーティング定義の修正とハンドラーの追加を行います。
code:resources/sample/config.edn
:sample.router/camelize-router
{:routes
:sample.handler.acme/camel-case
{}
:sample.handler.acme/post-json
{}
ハンドラーはあまり重要ではないので、JSONから:foo-bar-bazというキーを抜き出してレスポンスとして返却するようにしてみましょう。
code:src/sample/handler/acme.clj
(ns sample.handler.acme
;;………
(defmethod ig/init-key ::post-json _
(res/response (str (:foo-bar-baz body-params)))))
このままではまださっきのリクエストを送っても何も返せないのでミドルウェアを追加していきましょう。次のようにリクエストに作用するミドルウェアを書きます。もはや何も難しくないですね!
code:src/sample/middleware.clj
(ns sample.middleware
;;………
(handler (update req :body-params hyphenate-keys))))
(defmethod ig/init-key ::wrap-kebab-request _
wrap-kebab-request)
config.ednを修正してwrap-kebab-requestをミドルウェアに差込みます。さくさくっと進んでいきます。
code:resources/sample/config.edn
:duct.handler/root
{:middleware
^:demote [#ig/ref :sample.middleware/wrap-camelize-response
#ig/ref :sample.middleware/wrap-kebab-request]} :sample.middleware/wrap-camelize-response
{}
:sample.middleware/wrap-kebab-request
{}
APIを再起動して、最初に送ったリクエストをもう一度送ってみます。
code:sh
$ curl -XPOST 'localhost:3000/postJson' -H 'content-type: application/json' -d '{"fooBarBaz": 42}'
42
クエリパラメータ
私たちとしては以下のようにルーティング定義を書きたいわけです。今回/queryParamsというパスにcamelCaseParamというクエリパラメータが渡るようにしたいです。ですが、このままではリクエストする側は/queryParams?camel-case-param=42と書かねばなりません。
code:resources/sample/config.edn
:sample.router/camelize-router
{:routes
;;………
:sample.handler.acme/query-params
{}}
code:src/sample/handler/acme.clj
(ns sample.handler.acme
;;………
(defmethod ig/init-key ::query-params _
(fn [{:ataraxy/keys result :as req}] (let [camel-case-param result]
(res/response camel-case-param))))
ここまで読んでくれた方はもう察しがついているかと思いますが、最初に定義したルーターを修正していくことにします。Ataraxyの定義上シンボルはふたつ意味があるので、あまり簡素な実装をするとダメなんですがとりあえず目を瞑ることにします。以下のようにcamelize-routesを修正しました(あと?が最初にくるパターンもとりあえず無視しました)。 code:src/sample/router.clj
(postwalk
(cond
(string? x)
(str/join "/"))
(symbol? x)
(camel-case x :lower)
:else
x))
routes))
これでAPIを再起動すると…
code:sh
$ curl 'localhost:3000/queryParams?camelCaseParam=42'
42
完璧ですね!
まとめ
マイクロサービスの海にClojureで作ったAPIを隠すことに成功しました。これで誰もこのAPIがClojureで書かれているなんてわかりませんね(少なくとも外からは)。 余談にはなりますが、HTTPクライアントを使ってJSONを送るときや、返ってきたJSONをパースするときにも、inflectionsを使えば簡単にキーをlowerCamelCaseにしたりkebab-caseにすることができます。JSONシリアライザなどの機能にキーをモディファイするものもあるのでしっかり確認しておくと良いです。JDBCドライバのラッパーなどにもだいたい似たような機能がちゃんとあります。