Kotlin製Webアプリケーションフレームワーク Ktor の使い方3 〜リクエストパラメータとJSONレスポンス〜

f:id:Naotsugu:20201208213355p:plain

blog1.mammb.com

の続きです。


リクエストパスとパラメータ

前回までの例では、Routing にて単純な GET リクエストを処理しました。

POST など他のメソッドを受け付けるには以下のように、該当するHTTPメソッドに応答したもので受け付けるだけです。

fun Routing.root() {
    get("path") { }
    post("path") { }
}

パスは以下のように構造化して指定することもできます。

route("a") {
  route("b") {
     get {…}
     post {…}
  }
}

パスパラメータは以下のように利用できます。

get("/user/{login}") {
   val login = call.parameters["login"]
}

POST された内容は以下のように取得することができます。

post {
    val params = call.receiveParameters()
    val name = params["name"] ?: ""
}


入力フォームを作成する

前回と同様に HTML DSL にて POST 用の入力フォームを作成しましょう。

fun userForm(
        name: String = "",
        email: String = "",
        error: String = ""): HTML.() -> Unit = {
    head {
        //link(rel = "stylesheet", href = "/static/styles.css")
        link(rel = "stylesheet", href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css")
    }
    body {
        div(classes = "container") {
            if (error.isNotBlank()) {
                p(classes = "text-danger") { +error }
            }
            postForm (action = "/user/new", encType = FormEncType.applicationXWwwFormUrlEncoded) {
                div { +"Name:" }
                div { textInput(name = "name", classes = "form-control") { value = name } }
                div { +"Email:" }
                div { textInput(name = "email", classes = "form-control") { value = email } }
                br
                div { submitInput(classes = "btn btn-outline-primary") { value = "Register" }}
            }
        }
    }
}

CSS は bootstrap を使うようにしました。

以下のようなHTMLが生成されます。

<!DOCTYPE html>
<html>
  <head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      <form action="/user/new" enctype="application/x-www-form-urlencoded" method="post">
        <div>Name:</div>
        <div><input type="text" name="name" class="form-control" value=""></div>
        <div>Email:</div>
        <div><input type="text" name="email" class="form-control" value=""></div>
<br>
        <div><input type="submit" class="btn btn-outline-primary" value="Register"></div>
      </form>
    </div>
  </body>
</html>


POST リクエストを処理する

冒頭で見たように、POST時の処理を作成しましょう。

App.kt は以下のようになります。

package ktor.example

import io.ktor.application.*
import io.ktor.routing.*
import io.ktor.html.*
import io.ktor.http.content.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.get
import kotlinx.html.*

data class User(val name: String, val email: String)

fun Application.main() {
    routing {
        root()
    }
}

fun Routing.root() {
    route("/user/new") {
        get {
            call.respondHtml(block = userForm())
        }
        post {
            val params = call.receiveParameters()
            val name = params["name"] ?: ""
            val email = params["email"] ?: ""
            if (name.isBlank() || email.isBlank()) {
                call.respondHtml(block = userForm(name, email, "Name and Email is required."))
            } else {
                val user = User(name, email)
                call.respondText("OK [${user}]")
            }
        }
    }
}


実行します。

$ ./gradlew run

http://localhost:8080//user/new にアクセスしてみましょう。

f:id:Naotsugu:20201213125809p:plain

入力エラー時には以下のようになります。

f:id:Naotsugu:20201213125857p:plain


登録処理は今のところ、未実装なのでユーザ情報を表示するのみです。

f:id:Naotsugu:20201213125935p:plain


JSON を返却する

JSON を扱うには ContentNegotiation を install します。

GsonJackson、そして kotlinx.serialization が使えます。ここでは kotlinx.serialization を使います。

依存に io.ktor:ktor-serialization を追加するとともにプラグイン org.jetbrains.kotlin.plugin.serialization を使用します。

build.gradle.kt は以下のようになります。

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.4.20"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.4.20"
    application
}

repositories {
    jcenter()
}

dependencies {
    implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
    implementation("io.ktor:ktor-server-netty:1.4.3")
    implementation("io.ktor:ktor-html-builder:1.4.3")
    implementation("io.ktor:ktor-serialization:1.4.3")
    implementation("ch.qos.logback:logback-classic:1.2.3")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}

application {
    mainClass.set("io.ktor.server.netty.EngineMain")
}

Ktor で serialization を使うには、以下のように ContentNegotiation を構成します。

install(ContentNegotiation) {
    json()
}

返却するデータには @Serializable でアノテートします。

import kotlinx.serialization.Serializable

@Serializable
data class User(val name: String, val email: String)

call.respond() を呼べば JSON 形式でレスポンスされます。

val user = User(name, email)
call.respond(user)

f:id:Naotsugu:20201213171423p:plain


App.kt の全体は以下のようになりました。

package ktor.example

import io.ktor.application.*
import io.ktor.features.*
import io.ktor.routing.*
import io.ktor.html.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.get
import io.ktor.serialization.*
import kotlinx.html.*
import kotlinx.serialization.Serializable

@Serializable
data class User(val name: String, val email: String)

fun Application.main() {
    install(ContentNegotiation) {
        json()
    }
    routing {
        root()
    }
}

fun Routing.root() {
    route("/user/new") {
        get {
            call.respondHtml(block = userForm())
        }
        post {
            val params = call.receiveParameters()
            val name = params["name"] ?: ""
            val email = params["email"] ?: ""
            if (name.isBlank() || email.isBlank()) {
                call.respondHtml(block = userForm(name, email, "Name and Email is required."))
            } else {
                val user = User(name, email)
                call.respond(user)
            }
        }
    }
}

fun userForm(
        name: String = "",
        email: String = "",
        error: String = ""): HTML.() -> Unit = {
    head {
        //link(rel = "stylesheet", href = "/static/styles.css")
        link(rel = "stylesheet", href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css")
    }
    body {
        div(classes = "container") {
            if (error.isNotBlank()) {
                p(classes = "text-danger") { +error }
            }
            postForm (action = "/user/new", encType = FormEncType.applicationXWwwFormUrlEncoded) {
                div { +"Name:" }
                div { textInput(name = "name", classes = "form-control") { value = name } }
                div { +"Email:" }
                div { textInput(name = "email", classes = "form-control") { value = email } }
                br
                div { submitInput(classes = "btn btn-outline-primary") { value = "Register" }}
            }
        }
    }
}

次回に続く。

blog1.mammb.com