Suppose I have a scala case class with the ability to serialize into json (using json4s or some other library):
case class Weather(zip : String, temp : Double, isRaining : Boolean)
If I'm using a HOCON config file:
allWeather {
BeverlyHills {
zip : 90210
temp : 75.0
isRaining : false
}
Cambridge {
zip : 10013
temp : 32.0
isRainging : true
}
}
Is there any way to use typesafe config to automatically instantiate a Weather
object?
I'm looking for something of the form
val config : Config = ConfigFactory.parseFile(new java.io.File("weather.conf"))
val bevHills : Weather = config.getObject("allWeather.BeverlyHills").as[Weather]
The solution could leverage the fact that the value referenced by "allWeather.BeverlyHills"
is a json "blob".
I could obviously write my own parser:
def configToWeather(config : Config) =
Weather(config.getString("zip"),
config.getDouble("temp"),
config.getBoolean("isRaining"))
val bevHills = configToWeather(config.getConfig("allWeather.BeverlyHills"))
But that seems inelegant since any change to the Weather definition would also require a change to configToWeather
.
Thank you in advance for your review and response.
A simple solution without external libraries, inspired from playframework Configuration.scala
trait ConfigLoader[A] { self =>
def load(config: Config, path: String = ""): A
def map[B](f: A => B): ConfigLoader[B] = (config, path) => f(self.load(config, path))
}
object ConfigLoader {
def apply[A](f: Config => String => A): ConfigLoader[A] = f(_)(_)
implicit val stringLoader: ConfigLoader[String] = ConfigLoader(_.getString)
implicit val booleanLoader: ConfigLoader[Boolean] = ConfigLoader(_.getBoolean)
implicit val doubleLoader: ConfigLoader[Double] = ConfigLoader(_.getDouble)
}
object Implicits {
implicit class ConfigOps(private val config: Config) extends AnyVal {
def apply[A](path: String)(implicit loader: ConfigLoader[A]): A = loader.load(config, path)
}
implicit def configLoader[A](f: Config => A): ConfigLoader[A] = ConfigLoader(_.getConfig).map(f)
}
Usage:
import Implicits._
case class Weather(zip: String, temp: Double, isRaining: Boolean)
object Weather {
implicit val loader: ConfigLoader[Weather] = (c: Config) => Weather(
c("zip"), c("temp"), c("isRaining")
)
}
val config: Config = ???
val bevHills: Weather = config("allWeather.BeverlyHills")
Run the code in Scastie
typesafe config library has API to instantiate object from config that uses java bean convention. But as I understand case class does not follow those rules.
There are several scala libraries that wrap typesafe config and provide scala specific functionality that you are looking for.
For example using pureconfig reading config could look like
val weather:Try[Weather] = loadConfig[Weather]
where Weather
is a case class for values in config
Expanding on Nazarii's answer, the following worked for me:
import scala.beans.BeanProperty
//The @BeanProperty and var are both necessary
case class Weather(@BeanProperty var zip : String,
@BeanProperty var temp : Double,
@BeanProperty var isRaining : Boolean) {
//needed by configfactory to conform to java bean standard
def this() = this("", 0.0, false)
}
import com.typesafe.config.ConfigFactory
val config = ConfigFactory.parseFile(new java.io.File("allWeather.conf"))
import com.typesafe.config.ConfigBeanFactory
val bevHills =
ConfigBeanFactory.create(config.getConfig("allWeather.BeverlyHills"), classOf[Weather])
Follow up: based on the comments below it may be the case that only Java Collections, and not Scala Collections, are viable options for the parameters of the case class (e.g. Seq[T]
will not work).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With