只是为了说明如何避免使用 asInstanceOf 从类型化配置中获取值。
sealed trait Value extends Product with Serializable
final case class IntValue(value: Int) extends Value
final case class StringValue(value: String) extends Value
final case class BooleanValue(value: Boolean) extends Value
type Config = Map[String, Value]
sealed trait ValueExtractor[T] {
def extract(config: Config)(fieldName: String): Option[T]
}
object ValueExtractor {
implicit final val IntExtractor: ValueExtractor[Int] =
new ValueExtractor[Int] {
override def extract(config: Config)(fieldName: String): Option[Int] =
config.get(fieldName).collect {
case IntValue(value) => value
}
}
implicit final val StringExtractor: ValueExtractor[String] =
new ValueExtractor[String] {
override def extract(config: Config)(fieldName: String): Option[String] =
config.get(fieldName).collect {
case StringValue(value) => value
}
}
implicit final val BooleanExtractor: ValueExtractor[Boolean] =
new ValueExtractor[Boolean] {
override def extract(config: Config)(fieldName: String): Option[Boolean] =
config.get(fieldName).collect {
case BooleanValue(value) => value
}
}
}
implicit class ConfigOps(val config: Config) extends AnyVal {
def getAs[T](fieldName: String)(default: => T)
(implicit extractor: ValueExtractor[T]): T =
extractor.extract(config)(fieldName).getOrElse(default)
}
那么,你就可以这样使用了。
val config = Map("a" -> IntValue(10), "b" -> StringValue("Hey"), "d" -> BooleanValue(true))
config.getAs[Int](fieldName = "a")(default = 0) // res: Int = 10
config.getAs[Int](fieldName = "b")(default = 0) // res: Int = 0
config.getAs[Boolean](fieldName = "c")(default = false) // res: Boolean = false
现在,问题变成了如何从原始源创建类型化配置。
更好的是,如何将配置直接映射到 案例类。
但是,这些更复杂,最好只使用已经完成的东西,例如pureconfig。
作为一个学术练习,让我们看看我们是否可以支持Lists 和Maps。
让我们从列表开始,一种天真的方法是为列表值创建另一个案例类,并为每种列表创建一个提取器工厂(这个过程正式称为隐式派生) em>。
import scala.reflect.ClassTag
final case class ListValue[T](value: List[T]) extends Value
...
// Note that, it has to be a def, since it is not only one implicit.
// But, rather a factory of implicits.
// Also note that, it needs another implicit parameter to construct the specific implicit.
// In this case, it needs a ClasTag for the inner type of the list to extract.
implicit final def listExtractor[T: ClassTag]: ValueExtractor[List[T]] =
new ValueExtractor[List[T]] {
override def extract(config: Config)(fieldName: String): Option[List[T]] =
config.get(fieldName).collect {
case ListValue(value) => value.collect {
// This works as a safe caster, which will remove all value that couldn't been casted.
case t: T => t
}
}
}
现在,你可以这样使用了。
val config = Map("l" ->ListValue(List(1, 2, 3)))
config.getAs[List[Int]](fieldName = "l")(default = List.empty)
// res: List[Int] = List(1, 2, 3)
config.getAs[List[String]](fieldName = "l")(default = List("Hey"))
// res: String = List() - The default is not used, since the field is a List...
// whose no element could be casted to String.
但是,如果您需要其他泛型类型的列表,例如列表列表,则此方法仅限于普通类型。那么,这将不起作用。
val config = Map("l" ->ListValue(List(List(1, 2), List(3))))
val l = config.getAs[List[List[String]]](fieldName = "l")(default = List.empty)
// l: List[List[String]] = List(List(1, 2), List(3)) ???!!!
l.head
// res: List[String] = List(1, 2)
l.head.head
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
这里的问题是类型擦除,ClassTags不能解决,你可以尝试使用TypeTags,它可以保留完整的类型,但是解决起来比较麻烦。
对于 Maps,解决方案非常相似,尤其是如果您将密钥类型固定为 String(假设您真正想要的是嵌套配置)。但是,这篇文章现在太长了,所以我把它留给读者练习。
尽管如此,正如已经说过的,这很容易被破坏,并且不是完全健壮的。
有更好的方法,但我自己对这些(还)不是很熟练,即使我会,答案也会更长,根本没有必要。
幸运的是,即使 pureconfig 不直接支持 YAML,也有一个 module 支持,pureconfig-yaml。
我建议您看一下该模块,如果您还有其他问题,请直接提出一个新问题,标记 pureconfig 和 yaml。另外,如果只是小问题,可以尝试在gitter channel提问。