【问题标题】:How to create a custom decoder in Circe that parses time values如何在 Circe 中创建解析时间值的自定义解码器
【发布时间】:2021-06-29 05:15:30
【问题描述】:

我正在尝试将“5m”或“5s”或“5ms”形式的字符串解码为 FiniteDuration 类型的对象,它们分别为 5.minutes、5.seconds、5.milliseconds。

我正在尝试为涉及 FiniteDuration 类的项目创建自定义解码器和编码器。编码器没有问题,因为它只是读取 FiniteDuration 类的字段并生成一个字符串。但是,我在编写解码器时遇到了困难,我想知道我正在做的事情是否可行。

FiniteDuration 是一个具有如下构造函数的类:FiniteDuration(length: Long, unit: TimeUnit)。 Scala 附带了一些方便的语法糖,因此可以使用符号 5.minutes、5.seconds 或 5.milliseconds 来调用该类。在这种情况下,Scala 会为您创建 FiniteDuration 类。

想法是将这个 FiniteDuration 类转换为“5m”或“5s”或“5ms”之类的字符串,这样更容易阅读。

  implicit val d2json: Encoder[FiniteDuration] = new Encoder[FiniteDuration] {
    override def apply(a: FiniteDuration): Json = ???
  }

  implicit val json2d: Decoder[FiniteDuration] = new Decoder[FiniteDuration] {
    override def apply(c: HCursor): Decoder.Result[FiniteDuration] = ???
  }

我写的编码器应该没问题。解码器更棘手。我不确定该怎么做,因为 apply 方法需要 HCursor 类型的输入。

【问题讨论】:

    标签: json scala json-deserialization circe


    【解决方案1】:

    这是一个有效的基本实现(可能需要根据您对 FiniteDuration 的编码方式进行调整。

    基本上,您需要做的是将光标的值设为String,将该字符串拆分为持续时间和期间,并尝试将这两个部分分别转换为LongTimeUnit(因为@987654324 @构造函数接受它们作为参数)。

    请注意,这些转换必须返回 Either[DecodingFailure, _] 以与 cursor.as[_] 的返回类型保持一致,以便您可以在 for-comprehension 中使用它们。

    我对这些转换使用了隐式扩展方法,因为我觉得它们很方便,但您可以编写基本函数。

    implicit class StringExtended(str: String) {
        def toLongE: Either[DecodingFailure, Long] = {
          Try(str.toLong).toOption match {
            case Some(value) => Right(value)
            case None => Left(DecodingFailure("Couldn't convert String to Long", List.empty))
          }
        }
    
        def toTimeUnitE: Either[DecodingFailure, TimeUnit] = str match {
          case "ms" => Right(TimeUnit.MILLISECONDS)
          case "m" => Right(TimeUnit.MINUTES)
          // add other cases in the same manner
          case _ => Left(DecodingFailure("Couldn't decode time unit", List.empty))
        }
    }
    
    implicit val decoder: Decoder[FiniteDuration] = (c: HCursor) =>
      for {
        durationString <- c.as[String]
        duration <- durationString.takeWhile(_.isDigit).toLongE
        period = durationString.dropWhile(_.isDigit)
        timeUnit <- period.toTimeUnitE
      } yield {
        FiniteDuration(duration, timeUnit)
      }
    
    println(decode[FiniteDuration]("5ms".asJson.toString)) 
    // Right(5 milliseconds)
    

    【讨论】:

    • 谢谢,您的回答很有帮助。我唯一需要改变的是 FiniteDuration 不是一个案例类,所以它前面需要一个“新”。
    • 奇怪的是我的 sn-p 为我工作,运行 scala 2.12.8 和 circe 的最新稳定版本
    • 我刚刚检查了一下,代码编译并运行使用没有“new”的sbt。我的 Intellij 一直在其他地方表现出色,在这里它用红色强调了 FiniteDuration。看起来这只是 Intellij 的一个问题。再次感谢
    【解决方案2】:

    我猜您希望您的解析器符合 HOCON 标准? 然后您可以重用或复制 com.typesafe.config 库中使用的解析器。你需要的方法是

    public static long parseDuration(String input, ConfigOrigin originForException, String pathForException)

    【讨论】:

    • 我决定使用纯 Scala 实现,但您的回答非常有帮助。这个要求的想法是由一位同事提出的,他可能在其他地方看到过这个约定,这就是他提出这个建议的原因。不过,有问题的代码不会用于 HOCON。
    【解决方案3】:

    我认为解码FiniteDuration 的更好方法是使用现有类scala.concurrent.Duration,它是从标准库解析的:

    import io.circe.parser.decode
    import io.circe.{ CursorOp, Decoder, DecodingFailure, HCursor }
    import cats.syntax.validated._
    import cats.data.Validated
    
    import scala.concurrent.duration.{ Duration, FiniteDuration }
    import scala.language.postfixOps
    import scala.util.Try
    import scala.concurrent.duration._
    
    def parseDuration(ops: => List[CursorOp])
      (d: String): Either[DecodingFailure, FiniteDuration] =
      Validated
        .fromTry(Try(Duration(d)))
        .andThen {
          case _: Duration.Infinite     => new Exception("Field can not be infinite")
            .invalid[FiniteDuration]
          case duration: FiniteDuration => duration.valid[Throwable]
        }
        .leftMap(DecodingFailure.fromThrowable(_, ops))
        .toEither
    
    implicit val fDurationDecoder: Decoder[FiniteDuration] = (c: HCursor) => 
      c.as[String].flatMap(parseDuration(c.history))
    

    我从cats 添加了Validated,只是为了更舒适地处理错误并添加对无限输入的验证

    // tests
    decode[FiniteDuration](""""30 seconds"""") == Right(30 seconds)
    decode[FiniteDuration](""""{30 seconds"""") match {
      case Left(value) =>
        value match {
          case DecodingFailure(message, Nil) =>
            message.take(56) == """java.lang.NumberFormatException: For input string: "{30""""
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-25
      • 1970-01-01
      • 2017-05-16
      • 2023-03-04
      • 2018-09-15
      • 1970-01-01
      • 1970-01-01
      • 2020-05-31
      相关资源
      最近更新 更多