选择并使用适当的解码器作为新项目的解码器,同时保留错误累积

发布于 2025-01-26 15:54:11 字数 6640 浏览 6 评论 0原文

如果我有一些逻辑需要分歧的程度,因此需要选择一组可能使用的解码器中的1组,则可能会出现此问题。

考虑此代码段:

import cats.implicits.{catsSyntaxTuple3Semigroupal, catsSyntaxTuple2Semigroupal}
import io.circe.{Decoder, HCursor}

sealed trait Example

object Example {
  case class OptionOne(
    name: String,
    time: String,
    platform: String
  ) extends Example

  object OptionOne {
    implicit val decoder: Decoder[OptionOne] = {
      (
        Decoder[String].prepare(_.downField("event")),
        Decoder[String].prepare(_.downField("time")),
        Decoder[String].prepare(_.downField("platform"))
        )
        .mapN(OptionOne(_, _, _))
    }
  }

  case class OptionTwo(
    name: String,
    time: String
  ) extends Example

  object OptionTwo {
    implicit val decoder: Decoder[OptionTwo] = {
      (
        Decoder[String].prepare(_.downField("event")),
        Decoder[String].prepare(_.downField("time"))
        )
        .mapN(OptionTwo(_, _))
    }
  }

  implicit val decoder: Decoder[Example] = (a: HCursor) => {
    a.get[String]("event").flatMap {
      case "some_precise_name" => OptionTwo.decoder(a)
      case _ => OptionOne.decoder(a)
    }
  }
}

如果有条件地基于event值的值,我想拥有optionOneoptiontwo选择的选择的解码器代码>示例确实成功地做到了。此外,如果有问题,它确实会返回解码失败。但是,如果您希望错误积累,这就是问题出现的地方。

如果您考虑此JSON对象进行测试:

Json.obj(
  ("event", "randomEvent".asJson),
  ("time", 8.asJson),
  ("platform", 5.asJson)
  )

并将其传递到此基础:示例decoder.decodeaccumulating(InjsonInvalid.hcursor),只有第一个错误而不是预期的两个错误。

因此,问题是:如何以保留错误积累的方式完成?

[编辑]为了帮助澄清,这是一个测试套件,我希望通过解决方案来工作:

class ExampleSuite
  extends AnyFunSuite
    with Matchers
    with Inside {
  test("Decoder -- OptionOne") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson),
        ("context", Json.obj(
          ("app_version", "5.7.9".asJson)
          ))
        )
    noException should be thrownBy OptionOne.decoder.decodeJson(inJsonValidApp)
    inside(OptionOne.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionOne(name, time, platform)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
        platform should be("app")
    }

    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 8.asJson),
        ("platform", 5.asJson)
        )
    noException should be thrownBy OptionOne.decoder.decodeJson(inJsonInvalid)
    inside(OptionOne.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("String")
        ops should be(List(DownField("time")))
    }

    noException should be thrownBy OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)
    inside(OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
      case Invalid(NonEmptyList(head, tail)) =>
        inside(head) {
          case DecodingFailure(msg, ops) =>
            msg should be("String")
            ops should be(List(DownField("time")))
        }
        inside(tail) {
          case List(DecodingFailure(msg, ops)) =>
            msg should be("String")
            ops should be(List(DownField("platform")))
        }
    }
  }
  test("Decoder -- OptionTwo") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonValidApp)
    inside(OptionTwo.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionTwo(name, time)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
    }

    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("platform", "bee".asJson)
        )
    noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonInvalid)
    inside(OptionTwo.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("Attempt to decode value on failed cursor")
        ops should be(List(DownField("time")))
    }
  }

  test("Decoder -- Example (valid)") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonValidApp)
    inside(Example.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionOne(name, time, platform)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
        platform should be("app")
    }

    val inJsonBE: Json =
      Json.obj(
        ("event", "some_precise_name".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonBE)
    inside(Example.decoder.decodeJson(inJsonBE)) {
      case Right(OptionTwo(name, time)) =>
        name should be("some_precise_name")
        time should be("2021-05-05 20:09:57.448")
    }
  }
  test("Decoder -- Example (invalid, single)") {
    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 5.asJson),
        ("platform", "bee".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonInvalid)
    inside(Example.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("String")
        ops should be(List(DownField("time")))
    }
  }

  test("Decoder -- Example (invalid, multiple)") {
    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 8.asJson),
        ("platform", 5.asJson)
        )
    noException should be thrownBy Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)
    inside(Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
      case Invalid(NonEmptyList(head, tail)) =>
        inside(head) {
          case DecodingFailure(msg, ops) =>
            msg should be("String")
            ops should be(List(DownField("time")))
        }
        inside(tail) {
          case List(DecodingFailure(msg, ops)) =>
            msg should be("String")
            ops should be(List(DownField("platform")))
        }
    }
  }
}

If I have some point where my logic needs to diverge and thus have need to select 1 of some set of possible decoders to use, this issue can arise.

Consider this code snippet:

import cats.implicits.{catsSyntaxTuple3Semigroupal, catsSyntaxTuple2Semigroupal}
import io.circe.{Decoder, HCursor}

sealed trait Example

object Example {
  case class OptionOne(
    name: String,
    time: String,
    platform: String
  ) extends Example

  object OptionOne {
    implicit val decoder: Decoder[OptionOne] = {
      (
        Decoder[String].prepare(_.downField("event")),
        Decoder[String].prepare(_.downField("time")),
        Decoder[String].prepare(_.downField("platform"))
        )
        .mapN(OptionOne(_, _, _))
    }
  }

  case class OptionTwo(
    name: String,
    time: String
  ) extends Example

  object OptionTwo {
    implicit val decoder: Decoder[OptionTwo] = {
      (
        Decoder[String].prepare(_.downField("event")),
        Decoder[String].prepare(_.downField("time"))
        )
        .mapN(OptionTwo(_, _))
    }
  }

  implicit val decoder: Decoder[Example] = (a: HCursor) => {
    a.get[String]("event").flatMap {
      case "some_precise_name" => OptionTwo.decoder(a)
      case _ => OptionOne.decoder(a)
    }
  }
}

If conditionally based on the value of the event value I want to have either OptionOne or OptionTwo selected, the decoder for Example does successfully do this. Additionally, it does return the decoding failure if there is a problem. However, if you want the errors to accumulate, this is where the problem emerges.

If you consider this JSON object for testing:

Json.obj(
  ("event", "randomEvent".asJson),
  ("time", 8.asJson),
  ("platform", 5.asJson)
  )

and pass this in as such: Example.decoder.decodeAccumulating(inJsonInvalid.hcursor), only the first error is returned rather than both errors expected.

So the question is this: how can this be done in a way that preserves the accumulation of errors?

[EDIT] to help with clarification, here is a test suite I would expect to pass for the solution to work:

class ExampleSuite
  extends AnyFunSuite
    with Matchers
    with Inside {
  test("Decoder -- OptionOne") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson),
        ("context", Json.obj(
          ("app_version", "5.7.9".asJson)
          ))
        )
    noException should be thrownBy OptionOne.decoder.decodeJson(inJsonValidApp)
    inside(OptionOne.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionOne(name, time, platform)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
        platform should be("app")
    }

    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 8.asJson),
        ("platform", 5.asJson)
        )
    noException should be thrownBy OptionOne.decoder.decodeJson(inJsonInvalid)
    inside(OptionOne.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("String")
        ops should be(List(DownField("time")))
    }

    noException should be thrownBy OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)
    inside(OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
      case Invalid(NonEmptyList(head, tail)) =>
        inside(head) {
          case DecodingFailure(msg, ops) =>
            msg should be("String")
            ops should be(List(DownField("time")))
        }
        inside(tail) {
          case List(DecodingFailure(msg, ops)) =>
            msg should be("String")
            ops should be(List(DownField("platform")))
        }
    }
  }
  test("Decoder -- OptionTwo") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonValidApp)
    inside(OptionTwo.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionTwo(name, time)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
    }

    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("platform", "bee".asJson)
        )
    noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonInvalid)
    inside(OptionTwo.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("Attempt to decode value on failed cursor")
        ops should be(List(DownField("time")))
    }
  }

  test("Decoder -- Example (valid)") {
    val inJsonValidApp: Json =
      Json.obj(
        ("event", "something".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonValidApp)
    inside(Example.decoder.decodeJson(inJsonValidApp)) {
      case Right(OptionOne(name, time, platform)) =>
        name should be("something")
        time should be("2021-05-05 20:09:57.448")
        platform should be("app")
    }

    val inJsonBE: Json =
      Json.obj(
        ("event", "some_precise_name".asJson),
        ("time", "2021-05-05 20:09:57.448".asJson),
        ("platform", "app".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonBE)
    inside(Example.decoder.decodeJson(inJsonBE)) {
      case Right(OptionTwo(name, time)) =>
        name should be("some_precise_name")
        time should be("2021-05-05 20:09:57.448")
    }
  }
  test("Decoder -- Example (invalid, single)") {
    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 5.asJson),
        ("platform", "bee".asJson)
        )
    noException should be thrownBy Example.decoder.decodeJson(inJsonInvalid)
    inside(Example.decoder.decodeJson(inJsonInvalid)) {
      case Left(DecodingFailure(msg, ops)) =>
        msg should be("String")
        ops should be(List(DownField("time")))
    }
  }

  test("Decoder -- Example (invalid, multiple)") {
    val inJsonInvalid =
      Json.obj(
        ("event", "randomEvent".asJson),
        ("time", 8.asJson),
        ("platform", 5.asJson)
        )
    noException should be thrownBy Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)
    inside(Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
      case Invalid(NonEmptyList(head, tail)) =>
        inside(head) {
          case DecodingFailure(msg, ops) =>
            msg should be("String")
            ops should be(List(DownField("time")))
        }
        inside(tail) {
          case List(DecodingFailure(msg, ops)) =>
            msg should be("String")
            ops should be(List(DownField("platform")))
        }
    }
  }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

垂暮老矣 2025-02-02 15:54:11

由于它们是解码器,因此您可以将它们结合在一起:

implicit val decoder: Decoder[Example] = List[Decoder[Example]](
  Decoder[OptionOne].widen,
  Decoder[OptionTwo].widen,
  ...
).reduceLeft(_ or _)

它只会尝试每个

since they are Decoders, you can combine them in a such way:

implicit val decoder: Decoder[Example] = List[Decoder[Example]](
  Decoder[OptionOne].widen,
  Decoder[OptionTwo].widen,
  ...
).reduceLeft(_ or _)

it will just try each one

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文