scalafunctional-programminghoconpureconfig

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


I have following HOCON config:

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

HOCON represents paths "b.c.d" and "d.f.g" as objects. So, I would like to have a reader, which reads these configs as Map[String, String], ex:

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

I've created a reader and trying to do it recursively:

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
  })

It gives me the correct result but is there a way to avoid MutableMap here?

P.S. Also, I would like to keep implementation by "pureconfig" reader.


Solution

  • The solution given by Ivan Stanislavciuc isn't ideal. If the parsed config object contains values other than strings or objects, you don't get an error message (as you would expect) but instead some very strange output. For instance, if you parse a typesafe config document like this

    "a":[1]
    

    The resulting value will look like this:

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

    And even if the input only contains objects and strings, it doesn't work correctly, because it erroneously adds double quotes around all the string values.

    So I gave this a shot myself and came up with a recursive solution that reports an error for things like lists or null and doesn't add quotes that shouldn't be there.

      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) }
        }
      }
    

    Note that my solution doesn't mention ConfigValue or ConfigReader.Result at all. It only takes existing ConfigReader objects and combines them with combinators like map and orElse. This is, generally speaking, the best way to write ConfigReaders: don't start from scratch with methods like ConfigReader.fromFunction, use existing readers and combine them.

    It seems a bit surprising at first that the above code works at all, because I'm using reader within its own definition. But it works because the orElse method takes its argument by name and not by value.