レスポンスを返すPOSTメソッドもVapor4に対応させてみる

Leaf
レスポンスを返すPOSTメソッドもVapor4に対応させてみる

はじめに

POSTメソッドもVapor4に対応させるべく書き換えていきます。さまざまな資料をひっくり返して試行錯誤を繰り返した末、なんとかエラーを出さずに処理を行うことができるようになりました。

なお、ここで示す方法が正しいとは限らないので、公式のドキュメントも併せて参照してくださいね。

Vapor Docs
Vapors documentation (web framework for Swift).

POSTメソッドで変わるところ

routesメソッドの引数変更に対応する

Vapor3
router.post("api/book",String.parameter) { (req, book) -> Future<HTTPStatus> in
Vapor4
app.post("api","book",":bookID") { req -> EventLoopFuture<HTTPStatus> in

routesメソッドの引数が変わったので、それに関係するところを修正します。修正と言ってもrouterappに変わるだけです。

パラメータの受け取り方が変わっています

Vapor3
router.post("api/book",String.parameter) { (req, book) -> Future<HTTPStatus> in
Vapor4
app.post("api","book",":bookID") { req -> EventLoopFuture<HTTPStatus> in

先ほどと同じコードですが、パラメータの渡し方が変わっています。
Vapor3ではString.parameterで渡していましたが、Vapor4では名前付きラベル(という呼称だったような気がする)で渡します。
この場合では:bookIDですね。

このパラメータはリクエストに詰められたパラメータの中からラベルを指定して取り出すことができます。取り出すパラメータを明示的に示すことができ、大変わかりやすいです。

Vapor3
let id = try req.parameters.next(String.self).lowercased()
Vapor4
let id = (req.parameters.get("bookID") ?? "").lowercased()

なお、nilならば空文字を返すという安直な対応をしていますが、ケースバイケースの対応をお願いします。

レスポンスを返してもらう

いよいよ本丸です。POSTした後のサーバからのレスポンスを返してもらいます。MongoDBに対して処理を行い、その結果に応じてレスポンスを返してみます。

Vapor3ではrouter.postの戻り値をFuture<HTTPResponse>としてコーディングしていました。MongoDBへの追加ができると自動的にObjectIDが割り当てられるため、割り当てられたかどうかで追加の成否を判定する方針です。

Vapor3
let insertOneResult = try! collection.insertOne(book)
if let insertedId = insertOneResult?.insertedId {
    print("insertedId:"+(insertedId.stringValue ?? ""))
}
return .ok
let futureBool = insertOneResult.map { result -> HTTPResponseStatus in
    if let insertedId = result?.insertedID {
        print("insertedId:"+(insertedId.stringValue ?? ""))
        return .ok
    }
    else {
        return .notModified
    }
}
print(futureBool)
return futureBool

改めて見てみると、本当に割り当てられたIDが得られたかどうかで判定しているか怪しいですね。確か非同期処理のはずですが、非同期処理が終わる前にreturnしているような気配があります。

DB上には望んだデータが保存されていたので処理上は問題ないですが、追加処理とステータスコードを返す処理がバラバラに動いているように見えます。何らかの原因で追加されなくてもOK(200)ステータスが返ってきそう。

とはいえ、Vapor4に対応させる予定なので修正はしないですけどね!

さてVapor4ですが、The非同期という書き方です。insertOneで返された結果(insertOneResult)をmapにチェーンして処理します。

Vapor4
return collection.insertOne(book).map { insertRes -> HTTPResponseStatus in
    let insertedID = insertRes?.insertedID.objectIDValue?.description ?? ""

    if insertedID != "" {
        return .ok //insertなので.createを返すのも良さげ
    }
    //追加できなかったときは状況別に処理を分けたほうがいいかもしれない
    else {
        return .imATeapot //ティーポット(418)は実運用では避けたほうが無難ですよ
    }
}.hop(to: req.eventLoop)

末尾のhopはなくても動きそうに見えますが、EventLoopから取り出すというイメージですかね。

DBへの処理なしでHTTPStatusをポンッと返したい

ステータスコードだけをポンっと返すにはどうするのか?
使い所としては、妙なデータやパラメータを与えられて処理できない時にその旨回答するケースなどですかね。

Vapor4
return req.eventLoop.future().map { res -> HTTPStatus in
    return HTTPStatus.badRequest //返すステータスコードは適宜変更してください
}

EventLoop内で新しくFutureを作ってmapするということです。これが正しいかどうかはともかく、理屈としては正しそうです。

当然ですが、app.postの戻り値はEventLoopFuture<HTTPStatus>です。

小ネタ:ステータスコード418(私はティーポット)

20年以上前のエイプリルフールネタですね。諸々の議論の末、Unusedで生き残ったとのこと。

RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
This document describes HTCPCP, a protocol for controlling, monitoring, and diagnosing coffee pots. This memo provides information for the Internet community. I...

さすがGoogle。しっかりとノッています。

Error 418 (I’m a teapot)!?

まとめ

本の情報を削除したり更新したりするメソッドも同様の対応でOKです。

それにしても手こずりました。特にEventLoopFutureで。非同期処理のことをもっと深く知っておくべきでした。

よくよく考えれば(考えなくても)、非同期処理が終わる前にreturnで値を返せば、非同期処理で得られる値が欲しくても得られないのは当然な話でしたね。