Akka-HTTPを型で縛る


httpリクエスト/レスポンスでやり取りするStringな値をアプリケーションが期待するScalaの型に変換する方法について。

# リクエストを型で縛る

ユーザーからのリクエストをなるべく`String`として触らないようにする。

## URLパラメータ

以下のルーティングを考える。

```
GET /message?id=<ID>&body=<BODY>'
```

URLパラメータの`id`と`body`をなるべく`String`以外で扱う。
まず、型を用意する。

```scala
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を用意する。

```scala
val bodyUnmarshaller: Unmarshaller[String, AwesomeBody] =
  Unmarshaller.apply { (ec: ExecutionContext) => (s: String) => Future.successful(AwesomeBody(s)) }
```

次に、 URLパラメータから得られる`id=<ID>&body=<BODY>`を`Content`、つまり`EmptyMessage`か`Message`として扱えるようにするためにファクトリを実装する。

```scala
object Content {
  // ファクトリ
  def apply(id: Long, content: Option[AwesomeBody]): Content =
    content.map(Message(id, _)) getOrElse EmptyMessage(id)
}
```

Unmarshallerとファクトリがあれば以下のように書ける。

```scala
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]`を自前で用意しなければならない。

```scala
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が実装できる。

```scala
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を使用したい場合は以下のように書ける。

```scala
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](http://qiita.com/petitviolet/items/c5657d21485d6170cda3)

# レスポンスを型で縛る

オブジェクトをStringに変換すること無く、オブジェクトのまま返却する。
リクエストの逆で、Marshallerを用意すれば良い。

まずレスポンス用の型を実装する。
なおリクエストの例で使った型も再利用している。

```scala
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]`を実装する。

```scala
object ReplyJsonProtocol extends DefaultJsonProtocol {
  implicit val bodyFormat = jsonFormat1(AwesomeBody)
  implicit val replyFormat: RootJsonFormat[Reply] = jsonFormat2(Reply.apply)
}
```

これらだけで良い。
前述のrouteに組み合わせると以下のように書ける。

```scala
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!))"
  }
}
```