如何在 Circe 中创建解析时间值的自定义解码器

How to create a custom decoder in Circe that parses time values

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

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

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

我们的想法是将此 FiniteDuration class 转换为“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] = ???
  }

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

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

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

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

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

请注意,这些转换必须 return Either[DecodingFailure, _]cursor.as[_] 的 return 类型对齐,以便您可以在 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 的更好方法是使用现有的 class 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""""
    }
}