将 Hocon 配置读取为 Map[String, String] ,键以点表示法和值表示

Read Hocon config as a Map[String, String] with key in dot notation and value

我有以下 HOCON 配置:

a {
 b.c.d = "val1"
 d.f.g = "val2" 
}

HOCON 将路径“b.c.d”和“d.f.g”表示为对象。所以,我想要一个 reader,它将这些配置读取为 Map[String, String],ex:

Map("b.c.d" -> "val1", "d.f.g" -> "val2")

我已经创建了一个 reader 并尝试递归执行它:

import scala.collection.mutable.{Map => MutableMap}

  private implicit val mapReader: ConfigReader[Map[String, String]] = ConfigReader.fromCursor(cur => {
    def concat(prefix: String, key: String): String = if (prefix.nonEmpty) s"$prefix.$key" else key

    def toMap(): Map[String, String] = {
      val acc = MutableMap[String, String]()

      def go(
        cur: ConfigCursor,
        prefix: String = EMPTY,
        acc: MutableMap[String, String]
      ): Result[Map[String, Object]] = {
        cur.fluent.mapObject { obj =>
          obj.value.valueType() match {
            case ConfigValueType.OBJECT => go(obj, concat(prefix, obj.pathElems.head), acc)
            case ConfigValueType.STRING =>
              acc += (concat(prefix, obj.pathElems.head) -> obj.asString.right.getOrElse(EMPTY))
          }
          obj.asRight
        }
      }

      go(cur, acc = acc)
      acc.toMap
    }

    toMap().asRight
  })

它给了我正确的结果,但是 这里有没有办法避免 MutableMap?

P.S。另外,我想通过“pureconfig”reader.

继续实施

你可以不使用递归来做同样的事情。使用方法entrySet如下

import scala.jdk.CollectionConverters._

val hocon =
  """
  |a {
  | b.c.d = "val1"
  | d.f.g = val2 
  |}""".stripMargin
val config  = ConfigFactory.load(ConfigFactory.parseString(hocon))
val innerConfig = config.getConfig("a")

val map = innerConfig
  .entrySet()
  .asScala
  .map { entry =>
    entry.getKey -> entry.getValue.render()
  }
  .toMap

println(map)

生产

Map(b.c.d -> "val1", d.f.g -> "val2")

根据给定的知识,可以定义一个 pureconfig.ConfigReader 读作 Map[String, String] 如下

implicit val reader: ConfigReader[Map[String, String]] = ConfigReader.fromFunction {
  case co: ConfigObject =>
    Right(
      co.toConfig
        .entrySet()
        .asScala
        .map { entry =>
          entry.getKey -> entry.getValue.render()
        }
        .toMap
    )
  case value =>
    //Handle error case
    Left(
      ConfigReaderFailures(
        ThrowableFailure(
          new RuntimeException("cannot be mapped to map of string -> string"),
          Option(value.origin())
        )
      )
    )
}

Ivan Stanislavciuc 给出的解决方案并不理想。如果解析的配置对象包含字符串或对象以外的值,您不会收到错误消息(正如您所期望的那样),而是一些非常奇怪的输出。例如,如果你像这样解析一个类型安全的配置文档

"a":[1]

结果值将如下所示:

Map(a -> [
    # String: 1
    1
])

即使输入仅包含对象和字符串,它也无法正常工作,因为它错误地在所有字符串值周围添加了双引号。

所以我自己试了一下,想出了一个递归解决方案,该解决方案报告列表或 null 之类的错误,并且不添加不应该存在的引号。

  implicit val reader: ConfigReader[Map[String, String]] = {
    implicit val r: ConfigReader[String => Map[String, String]] =
      ConfigReader[String]
        .map(v => (prefix: String) => Map(prefix -> v))
        .orElse { reader.map { v =>
          (prefix: String) => v.map { case (k, v2) => s"$prefix.$k" -> v2 }
        }}
    ConfigReader[Map[String, String => Map[String, String]]].map {
      _.flatMap { case (prefix, v) => v(prefix) }
    }
  }

请注意,我的解决方案根本没有提及 ConfigValueConfigReader.Result。它只接受现有的 ConfigReader 对象,并将它们与 maporElse 等组合器组合起来。一般来说,这是编写 ConfigReaders 的最佳方式:不要从头开始使用 ConfigReader.fromFunction 之类的方法,使用现有的读取器并将它们结合起来。

乍一看上面的代码完全有效似乎有点令人惊讶,因为我在其自己的定义中使用了 reader。但它之所以有效,是因为 orElse 方法按名称而非按值获取参数。

我不想编写自定义阅读器来获取键值对的映射。我改为将我的内部数据类型从映射更改为成对列表(我使用的是 kotlin),然后如果需要,我可以在稍后的内部阶段轻松地将其更改为映射。我的HOCON就变成了这个样子

  additionalProperties = [
    {first = "sasl.mechanism", second = "PLAIN"},
    {first = "security.protocol", second = "SASL_SSL"},
  ]
  additionalProducerProperties = [
    {first = "acks", second = "all"},
  ]

对人类来说不是最好的...但我更喜欢它而不是必须构建自定义解析组件。