Configuration Sources

Configuration sources are represented in Ciris with ConfigSources, and are essentially functions which can retrieve values of type V in context F, for keys of type K, and which handle errors. More specifically, ConfigSources require a ConfigKeyType, which is a description of the type of keys the source supports. ConfigSources can then be expressed as K => ConfigEntry[F, K, V, V], and a simplified definition of ConfigSource is provided below.

import ciris.{ConfigEntry, ConfigKeyType}

{
  abstract class ConfigSource[F[_], K, V](val keyType: ConfigKeyType[K]) {
    def read(key: K): ConfigEntry[F, K, V, V]
  }
}

ConfigKeyType includes the name and type of the key. Ciris includes ConfigKeyTypes for keys used by sources supported in the core module, which include the following. You can easily create your own ConfigKeyTypes by specifying the name and type of the key.

ConfigKeyType.Argument
// res1: ciris.ConfigKeyType.Argument.type = Argument

ConfigKeyType.Environment
// res2: ciris.ConfigKeyType.Environment.type = Environment

ConfigKeyType.File
// res3: ciris.ConfigKeyType[(java.io.File, java.nio.charset.Charset)] = ConfigKeyType(file)

ConfigKeyType.Property
// res4: ciris.ConfigKeyType.Property.type = Property

ConfigKeyType[String]("custom key")
// res5: ciris.ConfigKeyType[String] = ConfigKeyType(custom key)

ConfigEntrys include the key which was retrieved, the ConfigKeyType, the unmodified source value retrieved from the ConfigSource, and the source value with additional transformations applied – for example, the result of attempting to decode the source value to type Int. A simplified definition of ConfigEntry is provided below.

import ciris.{ConfigError, ConfigValue}
import ciris.api.Apply

{
  final class ConfigEntry[F[_]: Apply, K, S, V] private (
    val key: K,
    val keyType: ConfigKeyType[K],
    val sourceValue: F[Either[ConfigError, S]],
    val value: F[Either[ConfigError, V]]
  ) extends ConfigValue[F, V]
}

Both the source value and the value might not be available, and in such cases, the ConfigError will provide more details. ConfigEntrys require that the context F has an Apply instance defined, to be able to transform the value, and to combine multiple values. Ciris provides convenience functions, like env, prop, and file (and envF, propF, and fileSync for suspending effects), which all make use of ConfigSources, and return ConfigEntrys. These convenience functions also attempts to decode the value with a configuration decoder, represented with ConfigDecoders. For example, here is how you could define env for reading environment variables.

import ciris.{ConfigDecoder, ConfigSource}
import ciris.api.Id

{
  def env[Value](key: String)(
    implicit decoder: ConfigDecoder[String, Value]
  ): ConfigEntry[Id, String, String, Value] = {
    ConfigSource.Environment
      .read(key)
      .decodeValue[Value]
  }
}

Ciris provides many convenience functions for creating ConfigSources in the companion object of ConfigSource. For more information on how to create additional ConfigSources, please refer to the supporting new sources section.

Source Transformations

Configuration sources can be transformed in different ways. Most notably, we can take an existing ConfigSource and suspend the reading of values into context G, by using suspendF, provided that there is a Sync instance defined for G, and a natural transformation F ~> G. This effectively means that we can take a synchronous impure configuration source, and make it pure with suspendF.

For example, system properties are mutable, and reading them is not pure by definition – since we can get different results for the same arguments, if the properties are modified in-between the function invocations. We can create a pure version of ConfigSource.Properties by making use of suspendF and, for example, IO from cats-effect.

import cats.effect.IO
// import cats.effect.IO

import ciris.cats.effect._
// import ciris.cats.effect._

ConfigSource.Properties.suspendF[IO]
// res8: ciris.ConfigSource[cats.effect.IO,String,String] = ConfigSource(Property)

The natural transformation Id ~> F (where F is IO here) exists as long as we have an Applicative[F] defined, which we have via the Sync[F] instance. If we take a look at propF, which is the pure version of prop, we’ll see that it’s also defined in a very similar fashion with suspendF.

import ciris.api.Sync

{
  def propF[F[_]: Sync, Value](key: String)(
    implicit decoder: ConfigDecoder[String, Value]
  ): ConfigEntry[F, String, String, Value] = {
    ConfigSource.Properties
      .suspendF[F]
      .read(key)
      .decodeValue[Value]
  }
}

If we simply want to transform the context of a ConfigSource, we can instead use transformF, which expects a natural transformation F ~> G, and that there is an Apply instance for G. The transformF function which is available on ConfigSource makes use of the similar transformF on ConfigEntry.

ConfigSource.Environment.transformF[IO]
// res10: ciris.ConfigSource[cats.effect.IO,String,String] = ConfigSource(Environment)

Sometimes it’s necessary to combine suspended reading and memoization in a context F, and the cats-effect module provides the suspendMemoizeF function on ConfigSource for this purpose. The function supports any F for which a Concurrent instance is available. The supporting new sources section provides an example of how the function can be used.