Вот немного другой подход:
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