Akka HTTPのバリデーションライブラリを作った


## 課題

- Akka HTTPを普通のAPIサーバーとして使う際に、リクエストのJSONが適切かどうかのバリデーションを掛けたい。
- 値のバリデーションはドメインロジックでやっても良いのだけど、そもそもマイナス値が入らないようにしたいとか、入力必須項目が埋まってないとかを返したい。

という事がありました。PlayだとFormにバリデーションのAPIがあるのですが、Akka HTTPでは無さそう＆調べても3rd partyライブラリとしてもあんまりなさそう。

ということで、試しに自分で作ってみました。

## 作ったもの

https://github.com/uryyyyyyy/akka-http-json-validation

バリデーションに違反しているリクエストを送ると、このようなレスポンスが返ってきます。

<img width="1009" alt="スクリーンショット 2018-12-01 11.29.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/7185/8834affd-11f1-6385-2cd5-818f919ad55b.png">

ネストしたオブジェクトやArrayにも対応しています。

レスポンスのdetailsのところには、送られたjsonの「どのKeyがどういう理由でダメなのか」をそれぞれ返してくれます。


## 作るにあたって考えたこと

### API仕様

PlayのValidationだと、返り値が

```json
{
  "obj1.obj2.column1[0]": "error.required"
}
```

のような形で返ってくるので、エラー内容を画面で扱うにはKeyの文字列をパースしないといけなくて使いにくかったので、今回はこのような形で返したかった。

```json
{
  "obj1": {
    "obj2": {
       "column1": {
         "0": "error.required"
       }
    }
  }
}
```

### コードの解説

大きさとしては100行もない小さなライブラリです。

#### Validation定義

JSONは、`Object`, `Array`がネストしており、最後にリテラル値が出てきます。
それらを扱うため、

```scala
trait ValidatorBase[T] {
  def validate(model: T): Option[JsValue]
}
trait ValidatorSeq[T] extends ValidatorBase[Seq[T]]
trait Validator[T] extends ValidatorBase[T]
```
をそれぞれ定義します。
そして、それぞれで再帰的にvalidateメソッドを呼ぶことでバリデーション結果が組み立てられ、最終的には一つの`Option[JsValue]`型が生成され、それをレスポンスに渡す形になります。

実際にユーザーのコードでバリデーションを定義するときには、
- どのKeyのバリデーションか
- どんなバリデーションを掛けるか
- 失敗した場合にどんなメッセージを出すか
といった内容を渡すことで定義します。

```scala
case class Tag(
  id: Int,
  text: String
)

object TagValidator extends Validator[Tag] {
  private def idRule: Validation =
    genValidation(
      "id",
      tag => tag.id <= 0,
      "id must be positive"
    )

  private def textRule: Validation =
    genValidation(
      "text",
      tag => tag.text.isEmpty,
      "text must not be empty"
    )

  val validations = Seq(idRule, textRule)
}
```

また、ネストしたオブジェクトに対してのバリデーションをしたければ、ネストした先のオブジェクトに対して、既に作ったValidatorを渡すことで対応します。

```scala
private def tagRule: Validation =
    genValidationInternal(
      "tag",
      nested => nested.tag,
      TagValidator
    )
```

#### Validation呼び出し

Akka HTTPのJSON Supportとやっていることは同じで、
- 自作クラスの変換（バリデーション）を定義して、呼び出し側でextendsする
- 変換（バリデーション）のAPIを呼ぶときに、implicit parameterとして上記で作った定義を渡す。

という形です。
https://doc.akka.io/docs/akka-http/current/common/json-support.html

```scala
trait CustomValidationDirectives extends ValidationDirectiveBase {
  implicit val tagV = TagValidator
  implicit val taglistV = TagListValidator
  implicit val nestedTagV = NestedTagValidator
}


object Main extends CustomValidationDirectives with CustomJsonFormat {

　　validate(as[Tag]) { tag => // JSON to validated Tag
  　...
　　}
```

`ValidationDirectiveBase`をextendsした自前のカスタムディレクティブを作ります。
呼び出し側では、それを継承することで、`validate` ディレクティブと、implicitにValidatorを渡す`as[T]`が使えるようになります。

ここで、`as[T]`はakka-http-spray-jsonの`as[T]`ではない事に注意です。

```scala
// akka-http-spray-json
def as[T](implicit um: FromRequestUnmarshaller[T]) = um

// akka-http-json-validation
def as[T](implicit um: FromRequestUnmarshaller[T], validator: ValidatorBase[T]): (FromRequestUnmarshaller[T], ValidatorBase[T]) = (um, validator)
```

`validate`ディレクティブでは、この「JSONからEntityへの変換定義」、「Entityのバリデーション定義」を使うことによって、validate済みのデータを返してくれます。

## まとめ

実装を通じて、Akka HTTPでのカスタムディレクティブの使い方や、implicitに注入されるものの振る舞いがわかりました。

短いコードなので、ぜひ読んでいただいてコメントいただけると嬉しいです。