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