2

这是一个简单的 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}

将 更改myValueOption开箱即用的作品,给出与上述代码相同的结果。但是,将其更改为scalaz.Maybe

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

结果是:

Response("HTTP/1.1 Status(400)"): {"message":"body 无法转换为 Test: CNil: El(DownField(myValue),true,false)."}

我应该如何实现所需的编码器/解码器?

4

2 回答 2

5

这是一种略有不同的方法:

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 实现你自己的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部分是允许我们将类似的东西解码{}为 a并按预期case class Foo(v: Maybe[Int])获得的魔法。Foo(Maybe.empty)这个名字有点混乱,但真正的意思是“即使最后一次操作失败,也要应用这个解码操作”。在解析的上下文中,例如像这样的案例类case class Foo(v: Maybe[Int]),最后一个操作将是尝试"v"在 JSON 对象中选择一个字段。如果没有"v"密钥,通常这就是故事的结局——我们的解码器甚至不会被应用,因为没有任何东西可以应用它。withReattempt无论如何,我们都可以继续解码。

这段代码非常低级,这些部分DecoderHCursorAPI 的设计更多是为了提高效率而不是为了用户友好,但如果你盯着它看,仍然可以知道发生了什么。如果上一次操作没有失败,我们可以检查当前 JSON 值是否为 null,Maybe.empty如果是则返回。如果不是,我们尝试将其解码为 an并在成功时A将结果包装进去。Maybe.just如果最后一个操作失败,我们首先检查操作和最后一个焦点是否不匹配(由于一些奇怪的极端情况,这个细节是必要的 - 请参阅我的建议链接的错误报告以获取详细信息)。如果他们不是,我们就空虚地成功。如果它们不匹配,我们就会失败。

同样,您几乎肯定不应该使用这个版本——映射Decoder[Option[A]]更清晰、更面向未来,而且效率略低。不过,理解withReattempt无论如何都是有用的。

于 2017-02-13T18:44:33.917 回答
2

这是一个可能的实现:

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)
}
于 2017-02-13T16:46:09.513 回答