Reloadable configs for Scala 3
Imagine a legacy config that follows no strict naming rules, having a mixture of kebab, camel and snake case keys.
Being able to put mixed-case config into a case class was the primary goal.
Secondly, if the whole config or part of it is fetched from a file or network, it would be great to refresh it once in a while and not restart the whole app.
- recon4s can read mixed-case configs
- supports hot-reload
- Defines
Configurable[T]
type class. Instances for commonly used Scala and Java types provided - Supports enums, trait families, default values
Reloadable[T]
trait marks hot-reloadable members- Adds no result wrappers. Throws exceptions (com.typesafe.config.ConfigException)
- Assumes case class fields follow camelCase naming and looks for kebab-case, camelCase, CamelCaps and snake_case config keys by default. Convention is configurable via given override
libraryDependencies += "io.github.ancane" %% "recon4s" % "0.4"
import recon4s.{*, given}
com.typesafe.config.Config
extention methods .as[T]
and .as[T]("path")
read and return an instance of type T.
import com.typesafe.config.*
import recon4s.{*, given}
case class AppConf(
appName: String,
appVersion: String = "0.1",
snakeBites: Boolean
) derives Configurable
val config = ConfigFactory.parseString(
"""|
| appName = recon4s
| app-version = "1.2.3"
| snake_bites = yes
|
|""".stripMargin
)
val appConfig: AppConf = config.as[AppConf]
watchConfig[T]
and watchConfigPath[T]("path")
functions read freshConfig
return instance of type T
and update each Reloadable[T]
encountered in T
every reloadInterval
and report success or failure to a corresponding callback.
import com.typesafe.config.*
import recon4s.{*, given}
import scala.concurrent.duration.*
case class AppConf(
appName: String,
featureFlag: Reloadable[Boolean]
) derives Configurable
val appConfig = watchConfig[AppConf](
freshConfig = ConfigFactory.parseString("{ include file(...) }"),
reloadInterval = 1.minute,
onReloadFailure = (e => println(s"reload failure: ${e.getMessage()}")),
onReloadSuccess = (path => println(s"$path reload success"))
)
// When `featureFlag` changes inside the included file,
// featureFlag.get() returns updated value
def flag = appConfig.featureFlag.get()
Conventions are:
- CamelToCebab
- CamelToCebabCamel
- CamelToCebabCamelCaps
- CamelToCebabCamelCapsSnake (default)
CamelToCebabCamelCapsSnake
means, that given fieldName
, recon4s will look for config key named field-name
or fieldName
or FieldName
or field_name
or as-is
in that order.
Switching to stricter convention:
given Convention = recon4s.CamelToCebab
Config keys direct override:
given Convention = recon4s.CamelToCebab.substitute("one", "TWO")
case class One(one: String)
ConfigFactory
.parseString("{ TWO = 1}")
.as[One]
given (using c: Configurable[String]): Configurable[LocalDate] =
c.map(LocalDate.parse(_, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
case class LocalDateConf(date: LocalDate)
val actual = ConfigFactory
.parseString("date = 2007/12/03")
.as[LocalDateConf]
type
key is used by default to get the type information when reading trait families or enums (with parameters).
Changing default type key:
given Convention = recon4s.CamelToCebab.withDescriminator("name")