Modules Overview

Ciris core module, ciris-core, provides basic functionality and readers for many standard library types. See Core Concepts for an explanation of the concepts in the core module. Remaining sections mostly cover the core module in greater detail, in particular Configuration Sources and Custom Readers. In the sections below, Ciris’ other modules are explained briefly.

Enumeratum

The ciris-enumeratum module provides support for reading enumeratum enumerations. You can refer to the documentation for a complete list of supported enumerations. As an example, let’s define an Enum and see how we can load values of that enumeration using Ciris. Enumeratum provides mixins, like Lowercase below, which we can use to customize the name of our enumerations, and in turn, what values we will be able to load with Ciris.

import enumeratum._
// import enumeratum._

import enumeratum.EnumEntry._
// import enumeratum.EnumEntry._

object configuration {
  sealed abstract class AppEnvironment extends EnumEntry with Lowercase
  object AppEnvironment extends Enum[AppEnvironment] {
    case object Local extends AppEnvironment
    case object Testing extends AppEnvironment
    case object Production extends AppEnvironment

    val values = findValues
  }
}
// defined object configuration

Let’s also define a custom implicit configuration source, holding some values we can ask Ciris to load.

import ciris._
// import ciris._

import ciris.enumeratum._
// import ciris.enumeratum._

import configuration._
// import configuration._

implicit val source = {
  val keyType = ConfigKeyType[String]("enumeratum key")
  ConfigSource.fromMap(keyType)(Map(
    "localEnv" -> "local",
    "testingEnv" -> "testing",
    "TestingEnv" -> "Testing",
    "invalidEnv" -> "invalid"
  ))
}
// source: ciris.ConfigSource[String] = ConfigSource(ConfigKeyType(enumeratum key))

We can then ask Ciris to load values of the enumeration, from the implicit source, using the read method.

read[AppEnvironment]("localEnv")
// res0: ciris.ConfigValue[configuration.AppEnvironment] = ConfigValue(Right(Local))

read[AppEnvironment]("testingEnv")
// res1: ciris.ConfigValue[configuration.AppEnvironment] = ConfigValue(Right(Testing))

read[AppEnvironment]("TestingEnv")
// res2: ciris.ConfigValue[configuration.AppEnvironment] = ConfigValue(Left(WrongType(TestingEnv, Testing, configuration$AppEnvironment, ConfigKeyType(enumeratum key), None)))

read[AppEnvironment]("invalidEnv")
// res3: ciris.ConfigValue[configuration.AppEnvironment] = ConfigValue(Left(WrongType(invalidEnv, invalid, configuration$AppEnvironment, ConfigKeyType(enumeratum key), None)))

Dealing with multiple environments is a common use case for configurations, explained in greater detail in the Multiple Environments section.

Generic

The ciris-generic module provides the ability to read unary products, and coproducts using shapeless. This allows you to load case classes with one argument, including value classes, and shapeless coproducts, plus anything else that shapeless’ Generic supports. Let’s take a brief look at these cases to see how it works in practice. We start by defining a source from which we can read configuration values.

import ciris._
// import ciris._

import ciris.generic._
// import ciris.generic._

implicit val source = {
  val keyType = ConfigKeyType[String]("generic key")
  ConfigSource.fromMap(keyType)(Map("key" -> "5.0"))
}
// source: ciris.ConfigSource[String] = ConfigSource(ConfigKeyType(generic key))

We can then define and load a unary product, for example a case class with one value.

final case class DoubleValue(value: Double)

read[DoubleValue]("key")

It also works for value classes and any other unary products shapeless’ Generic supports.

final class FloatValue(val value: Float) extends AnyVal

read[FloatValue]("key")

We can also define a shapeless coproduct and load it.

import shapeless.{:+:, CNil}

type DoubleOrFloat = DoubleValue :+: FloatValue :+: CNil

read[DoubleOrFloat]("key")

If we define a product with more than one value:

final case class TwoValues(value1: Double, value2: Float)
// defined class TwoValues

we will not be able to load it, resulting in an error at compile-time.

read[TwoValues]("key")
// <console>:22: error: could not find implicit value for parameter reader: ciris.ConfigReader[TwoValues]
//        read[TwoValues]("key")
//                       ^

Also, if there’s no public constructor or apply method:

object PrivateValues {
  final class PrivateFloatValue private(val value: Float) extends AnyVal
  object PrivateFloatValue {
    def withValue(value: Float) =
      new PrivateFloatValue(value)
  }
}
// defined object PrivateValues

import PrivateValues._
// import PrivateValues._

we will not be able to load it, again resulting in an error at compile-time.

read[PrivateFloatValue]("key")
// <console>:24: error: could not find implicit value for parameter reader: ciris.ConfigReader[PrivateValues.PrivateFloatValue]
//        read[PrivateFloatValue]("key")
//                               ^

Refined

The ciris-refined module allows you to load refinement types from refined. Let’s start by defining a configuration source from which to read some values.

import ciris._
// import ciris._

import ciris.refined._
// import ciris.refined._

implicit val source = {
  val keyType = ConfigKeyType[String]("refined key")
  ConfigSource.fromMap(keyType)(Map(
    "negative" -> "-1",
    "zero" -> "0",
    "positive" -> "1",
    "other" -> "abc"
  ))
}
// source: ciris.ConfigSource[String] = ConfigSource(ConfigKeyType(refined key))

In this example, we’ll simply try to read PosInt values, which are all Ints strictly greater than zero.

import eu.timepit.refined.types.numeric.PosInt
// import eu.timepit.refined.types.numeric.PosInt

read[PosInt]("negative")
// res0: ciris.ConfigValue[eu.timepit.refined.types.numeric.PosInt] = ConfigValue(Left(WrongType(negative, -1, eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Positive], ConfigKeyType(refined key), Some(Predicate failed: (-1 > 0).))))

read[PosInt]("zero")
// res1: ciris.ConfigValue[eu.timepit.refined.types.numeric.PosInt] = ConfigValue(Left(WrongType(zero, 0, eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Positive], ConfigKeyType(refined key), Some(Predicate failed: (0 > 0).))))

read[PosInt]("positive")
// res2: ciris.ConfigValue[eu.timepit.refined.types.numeric.PosInt] = ConfigValue(Right(1))

read[PosInt]("other")
// res3: ciris.ConfigValue[eu.timepit.refined.types.numeric.PosInt] = ConfigValue(Left(WrongType(other, abc, Int, ConfigKeyType(refined key), Some(java.lang.NumberFormatException: For input string: "abc"))))

Refinement types are useful for making sure your configuration is valid. See the Encoding Validation section for more information.

Squants

The ciris-squants module allows loading values with unit of measure from squants. For a complete list of supported dimensions, refer to the documentation. Let’s see how this works by first defining a configuration source with a few different keys mapping to Time values, each having a different unit of measure.

import squants.time.Time
// import squants.time.Time

import ciris._
// import ciris._

import ciris.squants._
// import ciris.squants._

implicit val source = {
  val keyType = ConfigKeyType[String]("squants key")
  ConfigSource.fromMap(keyType)(Map(
    "seconds" -> "3 s",
    "minutes" -> "23 m",
    "hours" -> "12 h"
  ))
}
// source: ciris.ConfigSource[String] = ConfigSource(ConfigKeyType(squants key))

We can then load these values by simply specifying that we want to read values of type Time.

val seconds = read[Time]("seconds")
// seconds: ciris.ConfigValue[squants.time.Time] = ConfigValue(Right(3.0 s))

val minutes = read[Time]("minutes")
// minutes: ciris.ConfigValue[squants.time.Time] = ConfigValue(Right(23.0 m))

val hours = read[Time]("hours")
// hours: ciris.ConfigValue[squants.time.Time] = ConfigValue(Right(12.0 h))

loadConfig(seconds, minutes, hours)(_ + _ + _).right.map(_.toMinutes)
// res0: scala.util.Either[ciris.ConfigErrors,Double] = Right(743.05)