kea

Summary

Codecov

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.

Cats friendly

Dependency Information

libraryDependencies += "com.waioeka" %% "kea-core" % "0.2.0"

Example

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"'
))

Optional values

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)), where a is the instance of A at the path.
  • Invalid(_), if the path exists but value could not be read (e.g. incorrect type).

Types

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.

Specifying a source for a configuration block.

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.