This library provides a type-safe (validated) way to query Typesafe configuration. The goal is to provide as minimal a wrapper as necessary, avoiding the direct use of macros, compiler plugins or a large number of dependencies.
Configuration values are returned as a ValidatedConfig[A]
, which is defined as Validated[NonEmptyList[Throwable],A]
.
So, any errors in your configuration may be accumulated.
Shapeless is used to read configuration into case classes, without the requirement for the library to directly use macros.
libraryDependencies += "com.waioeka" %% "kea-core" % "0.2.0"
We can specify the type of each configuration element, for example,
import com.typesafe.config.{Config, ConfigFactory}
import kea.implicits._
val config = ConfigFactory.load
config.as[String]("example.foo.some-string")
config.as[Int]("example.foo.some-int")
These return a ValidatedNel
, see cats for background details.
If we have configuration of the form:
adt {
a {
c: "hello"
d: 1
e: "world"
f: 2
}
b: 12
}
Then this can be read directly into the case class structure as follows:
case class Foo(c: String, d: Int, e: String, f: Int)
case class Bar(a: Foo, b: Int)
val result = config.as[Bar]("adt")
// result: (Valid(Bar(Foo(hello, 1, world, 2), 12)))
Note, by convention, given a field name abcDef
the configuration expected is abc-def
.
See, FieldNameMapper
.
Using a custom or generic configuration reader, any errors are accumulated as a non empty list of Throwable
.
For example, given:
val f = (config.as[String]("example.foo.some-string"),
config.as[Int]("first error"),
config.as[Boolean]("example.foo.some-boolean"),
config.as[Double]("second error"),
config.as[Long]("example.foo.some-long")).mapN(Foo.apply)
println(f)
Then f
will accumulate two errors:
Invalid(NonEmptyList(
com.typesafe.config.ConfigException$Missing: No configuration setting found for key '"first error"',
com.typesafe.config.ConfigException$Missing: No configuration setting found for key '"second error"'
))
Reading an optional value of type A
, for example,
config.as[Option[String]]("example.foo-somestring")
Returns a ValidatedNel[Option[A]]
, this will be:
Valid(None)
, if the path is missing (absence of the optional value).Validated(Some(a))
, wherea
is the instance ofA
at the path.Invalid(_)
, if the path exists but value could not be read (e.g. incorrect type).
Types are supported by implementing a ConfigReader
instance. An example implementation for ZonedDateTime
is shown below:
implicit val loadDateReader: ConfigReader[LocalDate] = (c: Config, p: String) =>
validated(LocalDate.parse(c.getString(p)))
The library itself implements ConfigReader
instances for the following types:
- primitives: String, Boolean, Int, Double, Long, BigInt, BigDecimal, Uuid, Url, Uri.
- configuration (reading inner configuration block): Config.
- date-time: ZonedDateTime, LocalData and LocalDateTime.
- enumerations: See the
EnumerationReaderTest
for a simple example. - case classes: support for algebraic data types.
Together with collection types (e.g. List
, Vector
, etc.) and Option
of the above.
In the example below the from
function reads a URL
from the configuration and sequentially
validates the configuration sourced.
The first parameter is the path of the URL within the top level config, the second parameter specifies the path to read within the configuration document.
val cfg =
"""
|consul = "http://127.0.0.1:8500/v1/kv/config/dev?raw=true"
""".stripMargin
val config = ConfigFactory.parseString(cfg)
case class Bar(a: String, b: Boolean, c: Int)
val conf = config.from[URL,Bar]("consul","example.bar")
// Valid(AppConfig(hello world,true,1234))
Sources can implement the ConfigFrom
trait, for example the library supplies a source for URL,
implicit def fromURL: ConfigFrom[URL] = (config: Conf, source: String) =>
config.as[URL](source).andThen(url => {
validated({
ConfigFactory.parseString(Source.fromURL(url).mkString)
})
})
With the from
function just adding the subsequent lookup,
def from[A,B](sourcePath: String, path: String)(implicit source: ConfigFrom[A],
reader: ConfigReader[B]): ValidatedConfig[B] =
source.from(this,sourcePath).andThen(cf => Conf(cf).as[B](path))
Ensuring we validate the type of the source and the underlying call to Source
.