Декодер Circe для скаляза.

Вот простой сервер зябликов, использующий circe в качестве декодера:

import com.twitter.finagle.http.RequestBuilder
import com.twitter.io.Buf
import io.circe.generic.auto._
import io.finch._
import io.finch.circe._

case class Test(myValue: Int)

val api = post("foo" :: body.as[Test]) { test: Test => Ok(test) }

val bodyPost = RequestBuilder()
  .url("http://localhost:8080/foo")
  .buildPost(Buf.Utf8("""{ "myValue" : 42 }"""))

api.toService.apply(bodyPost).onSuccess { response =>
  println(s"$response: ${response.contentString}")
}

// output: Response("HTTP/1.1 Status(200)"): {"myValue":42}

Замена myValue на Option работает по умолчанию, давая тот же результат, что и в приведенном выше коде. Однако, изменив его на scalaz.Maybe:

import scalaz.Maybe
case class Test(myValue: Maybe[Int])

приводит к:

Ответ ("HTTP / 1.1 Статус (400)"): {"message": "тело не может быть преобразовано в Test: CNil: El (DownField (myValue), true, false)."}

Как мне реализовать необходимый кодировщик / декодер?


person slouc    schedule 13.02.2017    source источник


Ответы (2)


Вот немного другой подход:

import io.circe.{ Decoder, Encoder }
import scalaz.Maybe

trait ScalazInstances {
  implicit def decodeMaybe[A: Decoder]: Decoder[Maybe[A]] =
    Decoder[Option[A]].map(Maybe.fromOption)

  implicit def encodeMaybe[A: Encoder]: Encoder[Maybe[A]] =
    Encoder[Option[A]].contramap(_.toOption)
}

object ScalazInstances extends ScalazInstances

А потом:

scala> import scalaz.Scalaz._, ScalazInstances._
import scalaz.Scalaz._
import ScalazInstances._

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> Map("a" -> 1).just.asJson.noSpaces
res0: String = {"a":1}

scala> decode[Maybe[Int]]("1")
res1: Either[io.circe.Error,scalaz.Maybe[Int]] = Right(Just(1))

Основное преимущество этой реализации (помимо того, что она более общая и даже немного более краткая) заключается в том, что она имеет поведение, которое вы обычно ожидаете от дополнительных членов в классах case. В вашей реализации, например, следующие входные данные не работают:

scala> import io.circe.generic.auto._
import io.circe.generic.auto._

scala> case class Foo(i: Maybe[Int], s: String)
defined class Foo

scala> decode[Foo]("""{ "s": "abcd" }""")
res2: Either[io.circe.Error,Foo] = Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(i))))

scala> decode[Foo]("""{ "i": null, "s": "abcd" }""")
res3: Either[io.circe.Error,Foo] = Left(DecodingFailure(Int, List(DownField(i))))

Хотя, если вы используете декодер выше, который просто делегирует декодеру Option, они декодируются в Empty:

scala> decode[Foo]("""{ "s": "abcd" }""")
res0: Either[io.circe.Error,Foo] = Right(Foo(Empty(),abcd))

scala> decode[Foo]("""{ "i": null, "s": "abcd" }""")
res1: Either[io.circe.Error,Foo] = Right(Foo(Empty(),abcd))

Конечно, решать вам, хотите вы такого поведения или нет, но это то, чего большинство людей, вероятно, ожидает от кодека Maybe.

Сноска

Одним из недостатков (в некоторых очень конкретных случаях) моего декодера является то, что он создает дополнительный Option для каждого успешно декодированного значения. Если вас очень беспокоит распределение (или вам просто интересно, как это работает, что, вероятно, является лучшей причиной), вы можете реализовать свое собственное на основе decodeOption circe:

import cats.syntax.either._
import io.circe.{ Decoder, DecodingFailure, Encoder, FailedCursor, HCursor }
import scalaz.Maybe

implicit def decodeMaybe[A](implicit decodeA: Decoder[A]): Decoder[Maybe[A]] =
  Decoder.withReattempt {
    case c: HCursor if c.value.isNull => Right(Maybe.empty)
    case c: HCursor => decodeA(c).map(Maybe.just)
    case c: FailedCursor if !c.incorrectFocus => Right(Maybe.empty)
    case c: FailedCursor => Left(DecodingFailure("[A]Maybe[A]", c.history))
  }

Часть Decoder.withReattempt - это магия, которая позволяет нам декодировать что-то вроде {} в case class Foo(v: Maybe[Int]) и получить Foo(Maybe.empty), как ожидалось. Название немного сбивает с толку, но на самом деле оно означает «применить эту операцию декодирования, даже если последняя операция не удалась». В контексте синтаксического анализа, например. для класса case, такого как case class Foo(v: Maybe[Int]), последней операцией будет попытка выбрать поле "v" в объекте JSON. Если нет "v" ключа, обычно это конец истории - наш декодер даже не применяется, потому что его не к чему применить. withReattempt позволяет нам в любом случае продолжить декодирование.

Этот код довольно низкоуровневый, и эти части Decoder и HCursor API предназначены больше для повышения эффективности, чем для удобства пользователя, но все же можно сказать, что происходит, если вы внимательно посмотрите на него. Если последняя операция не завершилась ошибкой, мы можем проверить, является ли текущее значение JSON нулевым, и вернуть Maybe.empty, если это так. Если это не так, мы пытаемся декодировать его как A и обертывать результат в Maybe.just, если это удается. Если последняя операция завершилась неудачно, мы сначала проверяем, не совпадают ли операция и последний фокус (деталь, которая необходима из-за некоторых странных угловых случаев - см. Мое предложение здесь и связанный отчет об ошибке подробнее). Если это не так, мы добьемся успеха. Если они не совпадают, мы проиграем.

Опять же, вам почти наверняка не следует использовать эту версию - отображение через Decoder[Option[A]] более четкое, более ориентированное на будущее и лишь немного менее эффективное. Однако понимание withReattempt может быть полезно в любом случае.

person Travis Brown    schedule 13.02.2017
comment
Итак, в основном вы говорите, что не делайте все самостоятельно, потому что в этом есть дыры, лучше делегировать существующему кодеку Option, который будет правильно обрабатывать такие вещи, как отсутствующие значения и нулевые значения. И я конечно согласен. Но если бы это был какой-то другой тип, который не изоморфен чему-то, что уже существует в circe.generic.auto._, ручной подход, такой как тот, что в моем ответе, все еще в порядке? (пока мне удастся залатать большинство дыр) - person slouc; 14.02.2017

Вот возможная реализация:

implicit def encodeDecodeMaybe: Encoder[Maybe[Int]] with Decoder[Maybe[Int]] = new Encoder[Maybe[Int]] with Decoder[Maybe[Int]] {
    override def apply(a: Maybe[Int]): Json = Encoder.encodeInt.apply(a.getOrElse(0)) // zero if Empty
    override def apply(c: HCursor): Decoder.Result[Maybe[Int]] = Decoder.decodeInt.map(s => Just(s)).apply(c)
}
person slouc    schedule 13.02.2017
comment
Проголосуйте за, но также посмотрите мой ответ, чтобы узнать подробности. :) - person Travis Brown; 13.02.2017
comment
@Travis Brown Я надеялся узнать подробности :) Большое спасибо! - person slouc; 13.02.2017