如何在 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
,将该字符串拆分为持续时间和句点,然后尝试将这两个部分都转换为 Long
和 TimeUnit
分别(因为 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""""
}
}
我正在尝试将“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
,将该字符串拆分为持续时间和句点,然后尝试将这两个部分都转换为 Long
和 TimeUnit
分别(因为 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""""
}
}