Circe декодер для скаляза. Может

Вот простой сервер Finch, использующий 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 Status(400)"): {"message":"тело не может быть преобразовано в Test: CNil: El(DownField(myValue),true,false)."}

Как мне реализовать нужный кодер / декодер?

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 для каждого успешно декодированного значения. Если вы крайне обеспокоены распределением ресурсов (или вам просто интересно узнать, как это работает, что, вероятно, является более веской причиной), вы можете реализовать свои собственные на основе Circe's decodeOption:

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 может быть полезным в любом случае, однако.

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

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)
}
Другие вопросы по тегам