scalaz.Maybe 的 Circe 解码器
Circe decoder for scalaz.Maybe
这是一个简单的 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])
结果:
Response("HTTP/1.1 Status(400)"): {"message":"body cannot be converted
to Test: CNil: El(DownField(myValue),true,false)."}
我应该如何实施所需的 encoder/decoder?
这是一个可能的实现:
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)
}
这里有一个稍微不同的方法:
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))
此实现的主要优点(除了它更通用甚至更简洁这一事实之外)是它具有您通常期望的可选成员在 classes 情况下的行为。例如,在您的实施中,以下输入失败:
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 的 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)
。这个名字有点令人困惑,但它真正的意思是"apply this decoding operation even if the last operation failed"。在解析的上下文中,例如像 case class Foo(v: Maybe[Int])
这样的案例 class,最后一个操作将是尝试 select JSON 对象中的 "v"
字段。如果没有 "v"
键,通常这将是故事的结尾——我们的解码器甚至不会被应用,因为没有任何东西可以应用它。 withReattempt
让我们继续解码。
这段代码很漂亮 low-level 并且 Decoder
和 HCursor
API 的这些部分比 user-friendliness 更注重效率,但仍然可以判断如果你盯着它看会发生什么。如果上次操作没有失败,我们可以检查当前JSON值是否为空,如果是returnMaybe.empty
。如果不是,我们尝试将其解码为 A
并在成功时将结果包装在 Maybe.just
中。如果最后一个操作失败,我们首先检查操作和最后一个焦点是否不匹配(由于一些奇怪的极端情况,这个细节是必要的——详见我的建议here and the linked bug report)。如果他们不是,我们就空洞地成功了。如果它们不匹配,我们就会失败。
同样,您几乎肯定不应该使用此版本 — Decoder[Option[A]]
上的映射更清晰,更 future-proof,而且效率略低。不过,了解 withReattempt
还是很有用的。
这是一个简单的 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])
结果:
Response("HTTP/1.1 Status(400)"): {"message":"body cannot be converted to Test: CNil: El(DownField(myValue),true,false)."}
我应该如何实施所需的 encoder/decoder?
这是一个可能的实现:
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)
}
这里有一个稍微不同的方法:
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))
此实现的主要优点(除了它更通用甚至更简洁这一事实之外)是它具有您通常期望的可选成员在 classes 情况下的行为。例如,在您的实施中,以下输入失败:
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 的 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)
。这个名字有点令人困惑,但它真正的意思是"apply this decoding operation even if the last operation failed"。在解析的上下文中,例如像 case class Foo(v: Maybe[Int])
这样的案例 class,最后一个操作将是尝试 select JSON 对象中的 "v"
字段。如果没有 "v"
键,通常这将是故事的结尾——我们的解码器甚至不会被应用,因为没有任何东西可以应用它。 withReattempt
让我们继续解码。
这段代码很漂亮 low-level 并且 Decoder
和 HCursor
API 的这些部分比 user-friendliness 更注重效率,但仍然可以判断如果你盯着它看会发生什么。如果上次操作没有失败,我们可以检查当前JSON值是否为空,如果是returnMaybe.empty
。如果不是,我们尝试将其解码为 A
并在成功时将结果包装在 Maybe.just
中。如果最后一个操作失败,我们首先检查操作和最后一个焦点是否不匹配(由于一些奇怪的极端情况,这个细节是必要的——详见我的建议here and the linked bug report)。如果他们不是,我们就空洞地成功了。如果它们不匹配,我们就会失败。
同样,您几乎肯定不应该使用此版本 — Decoder[Option[A]]
上的映射更清晰,更 future-proof,而且效率略低。不过,了解 withReattempt
还是很有用的。