circeをつかったバリデーションの実装

# はじめに

この記事はScala Advent Calendar 2018の12月12日の記事です。

アルプ株式会社でエンジニアをしていますshowmantです。

# 概要

今回取り組んだ内容の課題感としては本Advent Calendarの2日目にuryyyyyyyさんが書いてくださったことに似ています。


[Akka HTTPのバリデーションライブラリを作った](https://qiita.com/uryyyyyyy/items/83f5a0931f89d8c2e78e)

現在SaaSのプロダクトを開発中なのですが、いろいろな事情からPlay Frameworkを使わず、`Akka-http + circe + バリデーションライブラリー`で組むことにしました。

そこで「formのfieldバリデーション方法」が課題になりました。

# やりたかったこと

- web formから送信されたリクエストをAkka-httpで受け取り、受け取ったJSONを`circe`をつかってシリアライズし、フィールドのバリデーションしたい
- エラーが有った場合に、独自で定義したエラーオブジェクトに包んでレスポンスとして返したい

# 環境

- Scala 2.12系
- Akka-HTTP 10.1.5系
- circe 0.9.3
- cats 1.4.0

`circe`に対応したバリデーションライブラリはいくつかありました。

下記にあげさせていだきますm(_ _)m

[https://github.com/tabmo/circe-validation](https://github.com/tabmo/circe-validation)

[https://github.com/taig/circe-validation](https://github.com/taig/circe-validation)

今回は値のバリデーションロジックは極力自前で実装したくなかったため、tabmoさんのものを選択しました。

# 流れの説明

`Akka-HTTP`の実装手順に関しては今回は割愛しますが、イメージとしては下記のような流れです。

``` scala
    case class Person(
    firstName: String,
     lastName: String,
     age: Int,
     email: String)
    
    //独自に定義したErrorクラス。複数のfieldにエラーがあった場合はエラーを積んで返したい。
    case class FormError(errors: List[io.circe.Error]) extends Throwable
    
    class PersonController() {
      implicit val actorMaterializer: ActorMaterializer = ??? //略
      implicit val ec: ExecutionContext = ??? //略
    
      def routes: Route =
        path("person") {
          pathEndOrSingleSlash {
            post {
              extract(ctx => ctx.request) { request =>
                onComplete {
                  for {
                    result <- extractRequest(request)
                  } yield result
                } {
                  case Success(s) => s match {
                    case Right(person) => complete(person.firstName)
                    case Left(error) => failWith(error)
                  }
                  case Failure(exception) => failWith(exception)
                }
              }
    
            }
          }
        }
      def extractRequest(request: HttpRequest): Future[Either[FormError, Person]] =
        request.entity.dataBytes
          .runFold(ByteString.empty)(_ ++ _)
          .map(_.utf8String)
          .map(jsonToPerson)
    
      def jsonToPerson(stringJson: String): Either[FormError, Person] = {
        implicit def decoder: io.circe.Decoder[Person] = ???
    
        io.circe.parser.decodeAccumulating(stringJson) match {
          case Valid(value)    => Right(value)
          case Invalid(errors) => Left(FormError(errors.toList))
    
        }
      }
    
    }
```

ここで `???` となっている`Decoder`をどう実装するか、が課題になります。

# トライ 1

ひとまず、ライブラリにあるUsageを試してみました。

``` scala

    val decodePerson: Decoder[Person] = Decoder.instance[Person] { (c: Hcursor) =>  
      for {
        name        <- c.downField("name").read(StringRules.maxLength(32))
        lastName    <- c.downField("lastName").as[String]
        age         <- c.downField("age").read(IntRules.positive())
        email       <- c.downField("email").read(StringRules.email)
      } yield Person(name, lastName, age, email)
    }
```

記述もシンプルでかなり直感的に実装できます。

これでも十分なのですが、今回はどうしても、「全フィールドの値をチェックして配列で返したい」という要件を満たしたく、この実装だと、仮に `name` の長さが32字より大きくなってバリデーションに失敗すると、そこで処理が終了してしまい、後続のその他のフィールドに関しては探索してもらえないです。

故に`decodeAccumulating`を呼び出しても、エラーを蓄積してもらえません。

ちなみに`read(ルール)` の返り値は `Either[DecodingFailure, A]` となっていて、`Decoder.instance`メソッドは以下のようになっています。

``` scala

    type Result[A] = Either[DecodingFailure, A]
    final def instance[A](f: HCursor => Result[A]): Decoder[A] = new Decoder[A] {
        final def apply(c: HCursor): Result[A] = f(c)
      }
```

この`Result`がエラーを積んでいく仕組みになっていればいいんだな。と考えました。

# トライ2

そういうのありそうだなとライブラリを探し回っていると、`circe`のなかに`Decoder`クラスだけでなく、`AccumulationDecoder`クラスが存在していることを発見しました。

そちらのinstance関数は以下のとおりです。

``` scala

    final type Result[A] = ValidatedNel[DecodingFailure, A]
    //type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]
    
    final def instance[A](f: HCursor => Result[A]): AccumulatingDecoder[A] = new AccumulatingDecoder[A] {
        final def apply(c: HCursor): Result[A] = f(c)
      }

```

シグニチャを見る限り、これで行けそうだったので、簡単に下記のような実装イメージをまず考えました。

``` scala
    implicit def toValidatedNel[A](either: Either[DecodingFailure, A]): ValidatedNel[DecodingFailure, A] = ???
    implicit def decoder: AccumulatingDecoder[Person] = AccumulatingDecoder.instance[Person] { c: HCursor =>
        for {
          name        <- c.downField("name").read(StringRules.maxLength(32))
          lastName    <- c.downField("lastName").as[String]
          age         <- c.downField("age").read(IntRules.positive())
          email       <- c.downField("email").read(StringRules.email)
        } yield Person(name, lastName, age, email)
      }
```

なんかいけそうだなと思いました。が、あることに気づきました。すごく単純ですが、`AccumulatingDecoder`は`Decoder`クラスでもないし、`Decoder`クラスを継承しているわけでもないのです。

つまり、ほしかった`Decoder[A]`をつくることができないのですorz 

強烈な思わせぶりですね。。。。w(もしかしたらこの方式でうまくやる方法があるのかもしれませんが、今回は見つけられず。。ご存知の方いればご教示いただけると幸いですm(_ _)m)

# 最終的にできたこと

結果として、下記のコードでバリデーションをかけつつ、複数のfieldエラーを返却することができました。

``` scala
    def decoder: Decoder[Person] =
        (firstNameDecoder, lastNameDecoder, ageDecoder, emailDecoder).mapN(Person.apply)
      
      private[this] def firstNameDecoder: Decoder[String] =
        Decoder.instance(_.downField("user_id").read(StringRules.isNotEmpty()))
      private[this] def lastNameDecoder: Decoder[String] =
        Decoder.instance(_.downField("lastName").read(StringRules.maxLength(32)))
      private[this] def ageDecoder: Decoder[Int]      = Decoder.instance(_.downField("age").read(IntRules.positive()))
      private[this] def emailDecoder: Decoder[String] = Decoder.instance(_.downField("age").read(StringRules.email))
```

# 今後やりたいこと

### もうすこしシンプルに書く方法みつけたい

上記はシンプルなクラスなので、そこまで煩雑ではありませんが、JSONが入れ子になっていたりすると記述方法がやや冗長になります。

``` scala
case class Person(name: Name, age: Int)
  case class Name(firstName: String, lastName: String)
  def create: Decoder[Person] =
    (nameDecoder, ageDecoder).mapN(Person.apply)

  private[this] def nameDecoder: Decoder[Name] = Decoder.instance(_.get[Name]("name"))
  private[this] def ageDecoder: Decoder[Int]   = Decoder.instance(_.downField("age").read(IntRules.positive()))

  private[this] implicit def name: Decoder[Name] = (firstNameDecoder, lastNameDecoder).mapN(Name.apply)

  private[this] def firstNameDecoder: Decoder[String] =
    Decoder.instance(_.downField("first_name").read(StringRules.isNotEmpty()))
  private[this] def lastNameDecoder: Decoder[String] =
    Decoder.instance(_.downField("last_name").read(StringRules.notBlank()))
```

### 1フィールドに対して複数のバリデーションをかけたときに、両方を評価できるようにしたい

この方法だと1行に2つバリデーションを定義し、両方失敗したケースに最初のエラーしか返してくれません。

# まとめ

要は`Akka-HTTP` + `circe`で上記のような要件を満たしたいだけだったので、もし他によい方法があれば、教えていただけますとありがたいです。

書き終わってちょっと調べてみた感じ、`io.circe.Parser` にちょっと手を加えたら`AccumulatingDecoder`クラスの`Decoder`を実装しつつ、`Writer`でエラー蓄積ができるのではないかと考えています。

 この記事には間に合いませんでしたが、追って投稿します。