Safe Config provides a safe and convenient wrapper around Typesafe's Config library.
Add the following to your build.sbt
file:
libraryDependencies ++= Seq(
"com.kinja" %% "safe-config" % "1.1.6",
compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full))
Create your first config object:
import com.kinja.config.safeConfig
import play.api.Play.configuration.{ underlying => playConf }
@safeConfig(playConf)
object Config {
val dbConfig = for {
conf <- getConfig("db")
read <- conf.getString("read")
write <- conf.getString("write")
} yield DbConfig(read, write)
val languages = getStringList("application.languages")
val secret = getString("application.secret")
}
final case class DbConfig(readConnection : String, writeConnection : String)
// In Global.scala
override def onStart(app: Application): Unit = {
Config
...
}
The safeConfig
annotation marks a configuration object. Within the configuration object all errors are handled automatically and accessors are created exposing the pure values. Additionally, configuration objects expose the ConfigApi
interface (select "Visibility: All").
All config values in the configuration object are eagerly evaluated and if any are the wrong type or missing, an exception is thrown indicating the problems with reading the config file.
In order to catch these errors as soon as possible, you should reference your config objects during your application's startup.
As of version 1.1.0, Safe Config can be used to annotate a class as well. This works well with Play 2.4's dependency injection. Instead of passing the underlying play config to the macro directly, pass the name of the identifier it is available at within the class object.
import com.kinja.config.safeConfig
import play.api._, ApplicationLoader.Context
@safeConfig("rawConfig")
class Bootstrap(context: Context) extends BuiltInComponentsFromContext(context) {
private val rawConfig = configuration.underlying
val dbConfig = for {
conf <- getConfig("db")
read <- conf.getString("read")
write <- conf.getString("write")
} yield DbConfig(read, write)
val languages = getStringList("application.languages")
val secret = getString("application.secret")
}
In addition to the normal map
and flatMap
methods which allow you to use for
comprehensions in your configuration, Safe Config provides a <*>
operator which can be used to write shorter code.
val dbConfig = (BootupErrors((DbConfig.apply _).curried)
<*> conf.getString("db.read")
<*> conf.getString("db.write"))
The full API documentation is available here.
The example given above will expand to the following:
import com.kinja.config.safeConfig
import play.api.Play.configuration.{ underlying => playConf }
object Config extends com.kinja.config.ConfigApi {
import com.kinja.config._
val root = BootupErrors(LiftedTypesafeConfig(playConf))
private final class $Extractor(a : DbConfig, b : List[String], c : String)
private object $Extractor {
def construct : DbConfig => List[String] => String => $Extractor =
a => b => c => new $Extractor(a, b, c)
}
private val dbConfig = getConfig("db")
private val $orig_dbConfig : BootupErrors[DbConfig] = for {
conf <- dbConfig
read <- conf.getString("read")
write <- conf.getString("write")
} yield DbConfig(read, write)
private val $orig_languages : BootupErrors[List[String]] = getStringList("application.languages")
private val $orig_secret : BootupErrors[String] = getString("application.secret")
private val $Extractor_instance = (BootupErrors($Extractor.construct)
<*> $orig_dbConfig
<*> $orig_languages
<*> $orig_secret
).fold(errs => throw new BootupErrorsException(errs), a => a)
val dbConfig = $Extractor_instance.a
val languages = $Extractor_instance.b
val secret = $Extractor_instance.c
}
final case class DbConfig(readConnection : String, writeConnection : String)
Due to the way type-checking occurs within the macro, forward references are not allowed within the annotated object.
Because of a bug in Macro Paradise, annotation of objects nested within a class does not work.