Search code examples
scalaadtcase-classtypesafe-config

Parsing Typesafe configuration to a case class


What is a fitting case class to parse:

input {
    foo {
      bar = "a"
      baz = "b"
    }

    bar {
      bar = "a"
      baz = "c"
      other= "foo"
    }
}

from a typesafe HOCON configuration via https://github.com/kxbmap/configs?

How could this be read via an ADT? Looking at their example I am not sure how to build a class hierarchy where some classes have different number of fields - but could inherit some.

sealed trait Tree
case class Branch(value: Int, left: Tree, right: Tree) extends Tree
case object Leaf extends Tree

My sample here:

import at.tmobile.bigdata.utils.config.ConfigurationException
import com.typesafe.config.ConfigFactory
import configs.syntax._

val txt =
  """
    |input {
    |    foo {
    |      bar = "a"
    |      baz = "b"
    |      type = "foo"
    |    }
    |
    |    bar {
    |      bar = "a"
    |      baz = "c"
    |      other= "foo"
    |      type="bar"
    |    }
    |}
  """.stripMargin

val config = ConfigFactory.parseString(txt)
config

sealed trait Input{ def bar: String
  def baz:String }
case class Foo(bar: String, baz: String) extends Input
case class Bar(bar:String, baz:String, other:String)extends Input

config.extract[Input].toEither match {
  case Right(s) => s
  case Left(l) =>
    throw new ConfigurationException(
      s"Failed to start. There is a problem with the configuration: " +
        s"${l.messages.foreach(println)}"
    )
}

fails with:

No configuration setting found for key 'type'

Solution

  • If the input config will always consists of 2 fields (as in the example txt value; i.e. just foo and bar), then you can do it as such:

    val txt =
      """
        |input {
        |    foo {
        |      bar = "a"
        |      baz = "b"
        |      type = "foo"
        |    }
        |
        |    bar {
        |      bar = "a"
        |      baz = "c"
        |      other = "foo"
        |      type = "bar"
        |    }
        |}
      """.stripMargin
    
    sealed trait Input {
      def bar: String
      def baz: String
    }
    case class Foo(bar: String, baz: String) extends Input
    case class Bar(bar: String, baz: String, other:String) extends Input
    
    case class Inputs(foo: Foo, bar: Bar)
    
    val result = ConfigFactory.parseString(txt).get[Inputs]("input")
    println(result)
    

    Output:

    Success(Inputs(Foo(a,b),Bar(a,c,foo)))
    

    --

    If your intention is to setup a sequence of generic inputs, then you should reflect this in the config and parse for Seq[Input]:

    val txt =
      """
        |inputs = [
        |    {
        |      type = "Foo"
        |      bar = "a"
        |      baz = "b"
        |    }
        |
        |    {
        |      type = "Bar"
        |      bar = "a"
        |      baz = "c"
        |      other= "foo"
        |    }
        |]
      """.stripMargin
    
    sealed trait Input {
      def bar: String
      def baz: String
    }
    case class Foo(bar: String, baz: String) extends Input
    case class Bar(bar: String, baz: String, other: String) extends Input
    
    val result = ConfigFactory.parseString(txt).get[Seq[Input]]("inputs")
    println(result)    
    

    Output:

    Success(Vector(Foo(a,b), Bar(a,c,foo)))