Akka-Httpで良い感じにRequestとResponseを扱う


## 概要

最初、「WebAPIを作る」とかにしようと思ったのですがこちらの記事で紹介されてたので変えました。
[Akka-HttpでREST APIを作る](http://qiita.com/t-hiroyoshi/items/7f28f40f043e7abee3d0)


やりたかったことは、ちょっとしたWebAPIサーバを立てるときに、これまでだとsprayが選択肢に上がるのですが、これからはakka-httpだろうということで、試してみた次第です。

## ゴール

- akka-httpのDSLを使って便利に書く
- とはいえ、HTTPのRequestとResponseは柔軟に扱いたい。
- パスだけでなく、特定のヘッダがあるときだけ処理させるバリデーションをかけたい。
  - そして、そのバリデーションをいちいち明示的に書かなくても使いまわせるようにしたい。

## コード

```scala:build.sbt
lazy val akkaVersion = "2.4.9"

    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-http-core" % akkaVersion,
      "com.typesafe.akka" %% "akka-http-experimental" % akkaVersion,
      "com.typesafe.akka" %% "akka-http-testkit" % akkaVersion
    )
```

```scala:Main.scala
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer

import scala.concurrent.Future

object Main {
  def main(args: Array[String]): Unit = {

    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()
    implicit val ec = system.dispatcher

    val route = path("") {
      get {
        complete("ok")
      }
    } ~ get {
      pathPrefix("api") {
        path("execute" / IntNumber) { num =>
          Logic.validateCustomHeader{ _ =>
            req => Logic.execute(num, req)
          }
        }
      }
    }

    val bindingFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "127.0.0.1", 8080)

    println("Server online at http://127.0.0.1:8080/\nPress RETURN to stop...")
    
    //Enter押すと止まるようにしている。
    scala.io.StdIn.readLine()
    bindingFuture
      .flatMap(_.unbind())
      .onComplete(_ => system.terminate())
  }
}
```

`http://localhost:8080/api/execute/<int>`にgetコマンドを投げると、`Logic.validateCustomHeader`でheaderのチェックをしてから、`Logic.execute`の処理が行われるようにしました。

validateCustomHeaderはexecuteの処理の中に書いてもいいんですが、例えば認証系など全APIに共通の処理はこのように書いておくと忘れずに処理してくれるので良さげです。

Logicの中身は下記。

```scala:Logic.scala
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.RouteResult.Complete
import akka.http.scaladsl.server._

import scala.concurrent.{ExecutionContextExecutor, Future}

object Logic {

  val validateCustomHeader: Directive1[String] = {
    val value = headerValueByName("customHeaderKey")
    value.filter(_ == "customHeaderValue", MalformedHeaderRejection("customHeaderKey", "value was wrong"))
  }

  def execute(num: Int, req: RequestContext)(implicit ec: ExecutionContextExecutor): Future[RouteResult] = {
    Future{
      println(req.request.headers.mkString(","))
      val header1 = `Set-Cookie`(HttpCookie("key", "value"))
      val header2 = `Content-Type`(ContentTypes.`text/plain(UTF-8)`)
      val response = HttpResponse(StatusCodes.OK, List(header1, header2), HttpEntity(num.toString), HttpProtocols.`HTTP/1.1`)
      Complete(response)
    }
  }
}

```

`validateCustomHeader`ではリクエストのheaderに`customHeaderKey`があることと、その値が特定のものであることを確認しています。先ほど書いたような認証系にもってこいですね。

`execute`では、リクエストのヘッダをとりあえず確認してみたり、レスポンスにSet-CookieとかのHeaderを入れれることを確認しています。

最後にCompleteとするところはRejectedとする場合もあるっぽいんですが、自分で明示的に書かなきゃいけないことはないような気がします。

## 動作確認

### 正しいリクエスト

```
$ curl --verbose -H 'customHeaderKey:customHeaderValue' http://localhost:8080/api/execute/11
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/execute/11 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> customHeaderKey:customHeaderValue
> 
< HTTP/1.1 200 OK
< Set-Cookie: key=value
< Server: akka-http/2.4.9
< Date: Sat, 27 Aug 2016 13:43:40 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 2
< 
* Connection #0 to host localhost left intact
11
```

200で、Set-Cookieなども届いていることがわかる。

### URLでint以外が入ってきてる

```
$ curl --verbose -H 'customHeaderKey:customHeaderVale' http://localhost:8080/api/execute/aa
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/execute/aa HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> customHeaderKey:customHeaderVale
> 
< HTTP/1.1 404 Not Found
< Server: akka-http/2.4.9
< Date: Sat, 27 Aug 2016 13:42:25 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 42
< 
* Connection #0 to host localhost left intact
The requested resource could not be found.
```

Pathに合わないので404。

### headerに適切なKeyがない

```
$ curl --verbose http://localhost:8080/api/execute/11*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/execute/11 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Server: akka-http/2.4.9
< Date: Sat, 27 Aug 2016 13:42:58 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 57
< 
* Connection #0 to host localhost left intact
Request is missing required HTTP header 'customHeaderKey'
```

不適切なリクエストなので400`headerValueByName`がrejectを返している。
（エラーメッセージが詳細に語り過ぎな気がする。。。）

### headerの該当のKeyの値が違う

```
$ curl --verbose -H 'customHeaderKey:customHeaderVale' http://localhost:8080/api/execute/11
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/execute/11 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> customHeaderKey:customHeaderVale
> 
< HTTP/1.1 400 Bad Request
< Server: akka-http/2.4.9
< Date: Sat, 27 Aug 2016 13:42:04 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 73
< 
The value of HTTP header 'customHeaderKey' was malformed:
* Connection #0 to host localhost left intact
value was wrong
```

valueのフィルタに弾かれた場合はこの文言を返すように自分で書いたのでこうなる。

## まとめ

Akkaはドキュメントが多い割にはわかりにくいので、コード読みつつ書いてみました。