httpリクエスト/レスポンスでやり取りするStringな値をアプリケーションが期待するScalaの型に変換する方法について。
リクエストを型で縛る
ユーザーからのリクエストをなるべくString
として触らないようにする。
URLパラメータ
以下のルーティングを考える。
GET /message?id=<ID>&body=<BODY>'
URLパラメータのid
とbody
をなるべくString
以外で扱う。
まず、型を用意する。
sealed trait Content {
val id: Long
val response = complete(s"Requested: $this")
}
case class EmptyMessage(id: Long) extends Content
case class Message(id: Long, body: AwesomeBody) extends Content
case class AwesomeBody(value: String)
AwesomeBody
のコンストラクタにString
を受け付けるようになっている。
まずbody
のString
をAwesomeBody
に変換するためのUnmarshallerを用意する。
val bodyUnmarshaller: Unmarshaller[String, AwesomeBody] =
Unmarshaller.apply { (ec: ExecutionContext) => (s: String) => Future.successful(AwesomeBody(s)) }
次に、 URLパラメータから得られるid=<ID>&body=<BODY>
をContent
、つまりEmptyMessage
かMessage
として扱えるようにするためにファクトリを実装する。
object Content {
// ファクトリ
def apply(id: Long, content: Option[AwesomeBody]): Content =
content.map(Message(id, _)) getOrElse EmptyMessage(id)
}
Unmarshallerとファクトリがあれば以下のように書ける。
val route =
path("message") {
get {
parameters('id.as[Long], 'body.as(bodyUnmarshaller)?).as(Content.apply _) { c: Content =>
c.response
}
}
}
リクエストBody
上ではURLパラメータを扱ったが、次はBodyに入ったJSONについて。
メソッドはPOSTとして同じPath(/message
)とし、JSONは上に合わせてid
(必須)とbody
(任意)を持つとする。
つまり、以下の様なリクエストとなる。
curl 'http://localhost:8080/message' -H 'Content-Type: application/json' -d '{"id":99}'
curl 'http://localhost:8080/message' -H 'Content-Type: application/json' -d '{"id":100,"body":"hello"}'
まずはJSONからContent
へのUnmarshallerとなるRootJsonReader[Content]
を実装する。
今回のケースでは、DefaultJsonProtocol.jsonFormatN
を使ってEmptyMessage
とMessage
のRootJsonFormat
を用意してもだめで、
RootJsonFormat[Content]
を自前で用意しなければならない。
import spray.json.DefaultJsonProtocol
object ContentJsonProtocol extends DefaultJsonProtocol {
implicit val contentFormat = new RootJsonReader[Content] {
override def read(json: JsValue): Content =
json.asJsObject.getFields("id", "body") match {
case Seq(JsNumber(id)) => EmptyMessage(id.toLong)
case Seq(JsNumber(id), JsString(body)) => Message(id.toLong, AwesomeBody(body))
case _ => throw new DeserializationException("Content")
}
}
}
用意したRootJsonFormat[Content]
をimplicitで宣言しておけばentity(as[T])
とすればDirective1[T]
が得られ、以下のようにrouteが実装できる。
import ContentJsonProtocol.contentFormat
val route =
path("message") {
post {
entity(as[Content]) { c: Content =>
complete(s"pong: $c")
}
}
}
このrouteに対するリクエストとレスポンスはこのようになる。
$ curl 'http://localhost:8080/message' -H 'Content-Type: application/json' -d '{"id":99}'
pong: EmptyMessage(99)
$ curl 'http://localhost:8080/message' -H 'Content-Type: application/json' -d '{"id":100,"body":"hello"}'
pong: Message(100,AwesomeBody(hello))
ヘッダー
標準で用意されているヘッダー
この場合は簡単で、headerValueByType[T]
を使う。
T
として使用できる型はakka.http.scaladsl.model.headers.headers.scala
に定義されている。
例えばUser-Agentを使用したい場合は以下のように書ける。
val route =
path("header" / "ua") {
headerValueByType[`User-Agent`]() { userAgent =>
get {
complete(s"User-Agent => $userAgent")
}
}
}
このrouteに対するリクエストとレスポンスは以下のようになる。
$ curl localhost:8080/header/ua -A "Awesome-UA"
User-Agent => User-Agent: Awesome-UA
カスタムヘッダー
ボリュームが大きくなったので別記事
[Akka-HTTP]カスタムヘッダーの取り扱い方 - Qiita
レスポンスを型で縛る
オブジェクトをStringに変換すること無く、オブジェクトのまま返却する。
リクエストの逆で、Marshallerを用意すれば良い。
まずレスポンス用の型を実装する。
なおリクエストの例で使った型も再利用している。
case class AwesomeBody(value: String) // reuse
case class Reply(replyId: Long, body: AwesomeBody)
// factory
object Reply {
def apply(content: Content): Reply = Reply(
replyId = new Random().nextInt(100).toLong,
body = AwesomeBody(s"thank you! : $content")
)
}
次に、このReply
クラスに対するRootJsonProtocol[Reply]
を実装する。
object ReplyJsonProtocol extends DefaultJsonProtocol {
implicit val bodyFormat = jsonFormat1(AwesomeBody)
implicit val replyFormat: RootJsonFormat[Reply] = jsonFormat2(Reply.apply)
}
これらだけで良い。
前述のrouteに組み合わせると以下のように書ける。
import ReplyJsonProtocol._
val route =
path("message") {
post {
entity(as[Content]) { c: Content =>
complete(Reply(c))
}
}
}
complete
の引数にReply
オブジェクトを渡すだけでJSONにMarshallingし、application/json
にしてくれる。
$ curl -XPOST 'http://localhost:8080/message' -H 'Content-Type: application/json' -d '{"id":100,"body":"hello!"}'
HTTP/1.1 200 OK
Server: akka-http/2.4.2
Date: Sun, 08 May 2016 14:12:37 GMT
Content-Type: application/json
Content-Length: 97
{
"replyId": 74,
"body": {
"value": "thank you! : Message(100,AwesomeBody(hello!))"
}
}