Config
Installation
Section titled “Installation”or as a standalone dependency
Features
Section titled “Features”tofu-config
is a boilerplate-free way to load configuration files as Scala classes,
which provides:
- parallel error accumulation, which won’t give up parsing upon the first error;
- out-of-the box support for case classes and sealed hierarchies through
Magnolia
derivation, sugared withDerevo
macro-annotations; Typesafe Config
interoperability;- ease of integration with custom configuration sources and types.
Example
Section titled “Example”A simple groceries config:
{ "dairy": { "storeName": "Milkman's dream", "toBuy": ["eggs", "milk"] }, "beverages": { "storeName": "Teaworld", "toBuy": [{ "teaSort": "Pu'erh", }, { "coffeeSort": "Liberica" }] }}
might be parsed into the following structure:
import cats.syntax.show._import com.typesafe.config.ConfigFactoryimport derevo.deriveimport tofu.config.typesafe._
sealed trait Item
sealed trait Dairy extends Itemcase object eggs extends Dairycase object milk extends Dairy
sealed trait Beverage extends Itemcase class Tea(teaSort: String) extends Beveragecase class Coffee(coffeeSort: String) extends Beverage
case class ItemStore[I <: Item]( storeName: String, toBuy: List[I])
@derive(Configurable)case class Groceries( dairy: ItemStore[Dairy], beverages: ItemStore[Beverage])
val cfg = ConfigFactory.parseResources("groceries.conf")syncParseConfig[Groceries](cfg) match { case Left(errors) => errors.foreach(err => println(err.show)) case Right(groceries) => println(groceries)}
If we make a couple of errors in the config file, misspelling “eggs” and introducing beer type into beverages:
{ "dairy": { "storeName": "Milkman's dream", "toBuy": ["egs", "milk"] }, "beverages": { "storeName": "Teaworld", "toBuy": [{ "teaSort": "Pu'erh", }, { "coffeeSort": "Liberica" }, { "beerSort": "Sour" }] }}
we will encounter verbose parsing errors:
dairy.toBuy.[0] : bad string 'egs' : expected one of: eggs,milkbeverages.toBuy.[2] : no variant found
Abstractions
Section titled “Abstractions”tofu-config
comes with a couple of neat abstractions over config parsing.
Configurable
Section titled “Configurable”The typeclass, which defines the property of a specific type to be parsed from a config is called
Configurable
(the definition is simplified):
trait Configurable[A] { def apply[F[_]](cfg: ConfigItem[F]): F[A]}
where ConfigItem
is a set of possible types of config elements (analog of com.typesafe.ConfigValue
).
ParallelReader
Section titled “ParallelReader”The context bound which is essential for config parsing is ParallelReader
. It is defined as a wrapper for cats.Parallel
and inferred from it:
final case class ParallelReader[F[_]](paralleled: Parallel[ConfigT[F, *]])
For a config to be parsed into a value of type A
in the context of F
, the following instances should be provided:
def parseCfg[F[_]: Refs: MonadThrow: ParallelReader, A: Configurable](cfg: Config): F[A]
MonadThrow
is for raising parsing errors.
ParallelReader
is for parallel parsing.
Refs
is for storing errors list inside a Ref
.
Custom types
Section titled “Custom types”Sometimes one needs to define custom ways to parse values from a config. For example, we might want to provide a convenient syntax for request limits:
{ "endpointA": "10 per 1 second", "endpointB": "1000 per 1 hour"}
It can be done via a custom Configurable
instance:
import com.typesafe.config.ConfigFactoryimport derevo.deriveimport tofu.config.ConfigError.BadStringimport tofu.config.typesafe._import tofu.syntax.raise._
import scala.concurrent.duration.{Duration, FiniteDuration}import scala.util.Try
case class Limit(count: Int, duration: FiniteDuration)
val limitRegexp = "(\\d+) per ([0-9a-z ]+)".r
def fromString(string: String): Option[Limit] = string match { case limitRegexp(count, duration) => Try(Limit(count.toInt, FiniteDuration(Duration(duration).toSeconds, "seconds"))).toOption case _ => None}
implicit val rateConfigurable: Configurable[Limit] = Configurable.stringConfigurable.flatMake { F => str => fromString(str).orRaise(BadString(str, "Invalid limit rate"))(F.config, F.monad)}
@derive(Configurable)case class Limits(endpointA: Limit, endpointB: Limit)
println(syncParseConfig[Limits](ConfigFactory.parseResources("limits.conf")))
Custom config sources
Section titled “Custom config sources”In order to provide a way to parse config from a custom source, one needs to provide a
mapping from that source’s types into tofu.config.ConfigItem
.
For an example, please refer to the typesafe integration
.