Combinators

The combinators defined in ConfigReader provide an easy way to create new ConfigReader instances by transforming existing ones. They are the simplest solution for supporting new simple types and for slightly modifying existing implementations, since the amount of boilerplate required is very small. This section contains some examples of combinators and shows how to work with them in PureConfig.

The simplest combinator is map, which simply transforms the result of an existing reader:

import pureconfig._
import pureconfig.generic.auto._

case class BytesConf(bytes: Vector[Byte])

// reads an array of bytes from a string
implicit val byteVectorReader: ConfigReader[Vector[Byte]] =
  ConfigReader[String].map(_.getBytes.toVector)
ConfigSource.string("""{ bytes = "Hello world" }""").load[BytesConf]
// res0: ConfigReader.Result[BytesConf] = Right(
//   BytesConf(Vector(72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100))
// )

emap allows users to validate the inputs and provide detailed failures:

import pureconfig.error._

case class Port(number: Int)
case class PortConf(port: Port)

// reads a TCP port, validating the number range
implicit val portReader = ConfigReader[Int].emap {
  case n if n >= 0 && n < 65536 => Right(Port(n))
  case n => Left(CannotConvert(n.toString, "Port", "Invalid port number"))
}
ConfigSource.string("{ port = 8080 }").load[PortConf]
// res1: ConfigReader.Result[PortConf] = Right(PortConf(Port(8080)))
ConfigSource.string("{ port = -1 }").load[PortConf]
// res2: ConfigReader.Result[PortConf] = Left(
//   ConfigReaderFailures(
//     ConvertFailure(
//       CannotConvert("-1", "Port", "Invalid port number"),
//       Some(ConfigOrigin(String)),
//       "port"
//     ),
//     WrappedArray()
//   )
// )

ensure allows users to quickly fail a reader if a condition does not hold:

import pureconfig.generic.semiauto._

case class Bounds(min: Int, max: Int)

implicit val boundsReader = deriveReader[Bounds]
  .ensure(b => b.max > b.min, _ => "Max must be bigger than Min")
ConfigSource.string("{ min = 1, max = 3 }").load[Bounds]
// res3: ConfigReader.Result[Bounds] = Right(Bounds(1, 3))
ConfigSource.string("{ min = 5, max = 3 }").load[Bounds]
// res4: ConfigReader.Result[Bounds] = Left(
//   ConfigReaderFailures(
//     ConvertFailure(
//       UserValidationFailed("Max must be bigger than Min"),
//       Some(ConfigOrigin(String)),
//       ""
//     ),
//     WrappedArray()
//   )
// )

orElse can be used to provide alternative ways to load a config:

val csvIntListReader = ConfigReader[String].map(_.split(",").map(_.toInt).toList)
implicit val intListReader = ConfigReader[List[Int]].orElse(csvIntListReader)

case class IntListConf(list: List[Int])
ConfigSource.string("""{ list = [1,2,3] }""").load[IntListConf]
// res5: ConfigReader.Result[IntListConf] = Right(IntListConf(List(1, 2, 3)))
ConfigSource.string("""{ list = "4,5,6" }""").load[IntListConf]
// res6: ConfigReader.Result[IntListConf] = Right(IntListConf(List(4, 5, 6)))