Adds support for selected scalaz data structures to PureConfig, provides instances of
scalaz
type classes for ConfigReader
, ConfigReaderFailures
, ConfigWriter
and ConfigConvert
and some syntactic sugar for pureconfig
classes.
In addition to core pureconfig, you'll need:
libraryDependencies += "com.github.pureconfig" %% "pureconfig-scalaz" % "0.17.4"
The following scalaz
data structures are supported:
IList
,ISet
,Maybe
,NonEmptyList
and==>>
Order[A]
should also be in scope, when you're relying on eitherConfigReader[A ==>> B]
orConfigReader[ISet[A]]
. For example, if yourISet
instance containsString
values thenOrder[String]
can be imported viascalaz.std.string._
Here is an usage example:
import com.typesafe.config.ConfigFactory.parseString
import pureconfig._
import pureconfig.generic.auto._
import pureconfig.module.scalaz._
import scalaz.{==>>, IList, ISet, Maybe, NonEmptyList}
import scalaz.std.anyVal.intInstance
import scalaz.std.string._
case class ScalazConfig(
numberLst: IList[Int],
numberSet: ISet[Int],
numberNel: NonEmptyList[Int],
numberMap: String ==>> Int,
numberMaybe: Maybe[Int]
)
We can read a ScalazConfig
like:
val scalazConf = parseString("""{
number-lst: [1,2,3],
number-set: [1,2,3],
number-nel: [1,2,3],
number-map { "one": 1, "two": 2, "three": 3 },
number-maybe: 1
}""")
// scalazConf: com.typesafe.config.Config = Config(SimpleConfigObject({"number-lst":[1,2,3],"number-map":{"one":1,"three":3,"two":2},"number-maybe":1,"number-nel":[1,2,3],"number-set":[1,2,3]}))
ConfigSource.fromConfig(scalazConf).load[ScalazConfig]
// res0: ConfigReader.Result[ScalazConfig] = Right(
// ScalazConfig(
// ICons(1, ICons(2, ICons(3, []))),
// Bin(2, Bin(1, Tip(), Tip()), Bin(3, Tip(), Tip())),
// NonEmpty[1,2,3],
// Bin("three", 3, Bin("one", 1, Tip(), Tip()), Bin("two", 2, Tip(), Tip())),
// Just(1)
// )
// )
In order to put in scope scalaz
type classes for our readers and extend them with the extra
operations provided by scalaz
, we need some extra imports:
import pureconfig.module.scalaz.instances._
import scalaz._
import scalaz.Scalaz._
We are now ready to use the new syntax:
case class SimpleConfig(i: Int)
// a reader that always returns SimpleConfig(42)
val constReader = SimpleConfig(42).point[ConfigReader]
// a reader that returns SimpleConfig(-1) if an error occurs
val safeReader = ConfigReader[SimpleConfig].handleError(_ => SimpleConfig(-1).point[ConfigReader])
And we can finally put them to use:
val validConf = parseString("""{ i: 1 }""")
// validConf: com.typesafe.config.Config = Config(SimpleConfigObject({"i":1}))
val invalidConf = parseString("""{ s: "abc" }""")
// invalidConf: com.typesafe.config.Config = Config(SimpleConfigObject({"s":"abc"}))
constReader.from(validConf.root())
// res1: ConfigReader.Result[SimpleConfig] = Right(SimpleConfig(42))
constReader.from(invalidConf.root())
// res2: ConfigReader.Result[SimpleConfig] = Right(SimpleConfig(42))
safeReader.from(validConf.root())
// res3: ConfigReader.Result[SimpleConfig] = Right(SimpleConfig(1))
safeReader.from(invalidConf.root())
// res4: ConfigReader.Result[SimpleConfig] = Right(SimpleConfig(-1))
In case there's a necessity to parse multiple configs and accumulate errors, you could leverage from Semigroup
instance for ConfigReaderFailures
:
val anotherInvalidConf = parseString("""{ i: false }""")
// anotherInvalidConf: com.typesafe.config.Config = Config(SimpleConfigObject({"i":false}))
List(validConf, invalidConf, anotherInvalidConf).traverse { c =>
Validation.fromEither(implicitly[ConfigReader[SimpleConfig]].from(c.root))
}
// res5: Validation[error.ConfigReaderFailures, List[SimpleConfig]] = Failure(
// ConfigReaderFailures(
// ConvertFailure(KeyNotFound("i", Set()), Some(ConfigOrigin(String)), ""),
// ArrayBuffer(
// ConvertFailure(
// WrongType(BOOLEAN, Set(NUMBER)),
// Some(ConfigOrigin(String)),
// "i"
// )
// )
// )
// )
We can provide some useful extension methods by importing:
import pureconfig.module.scalaz.syntax._
For example, you can easily convert a ConfigReaderFailures
to a NonEmptyList[ConfigReaderFailure]
:
case class MyConfig(i: Int, s: String)
val myConf = parseString("{}")
// myConf: com.typesafe.config.Config = Config(SimpleConfigObject({}))
val res = ConfigSource.fromConfig(myConf).load[MyConfig].left.map(_.toNel)
// res: Either[NonEmptyList[error.ConfigReaderFailure], MyConfig] = Left(
// NonEmpty[ConvertFailure(KeyNotFound(i,Set()),Some(ConfigOrigin(String)),),ConvertFailure(KeyNotFound(s,Set()),Some(ConfigOrigin(String)),)]
// )
This allows scalaz
users to easily convert a result of a ConfigReader
into a ValidatedNel
:
import scalaz.{ Validation, ValidationNel }
import pureconfig.error._
val result: ValidationNel[ConfigReaderFailure, MyConfig] =
Validation.fromEither(res)
// result: ValidationNel[ConfigReaderFailure, MyConfig] = Failure(
// NonEmpty[ConvertFailure(KeyNotFound(i,Set()),Some(ConfigOrigin(String)),),ConvertFailure(KeyNotFound(s,Set()),Some(ConfigOrigin(String)),)]
// )
Also, you could create ConfigReader
s using scalaz
types:
case class Tweet(msg: String)
val tweetReader: ConfigReader[Tweet] = ConfigReader.fromNonEmptyStringDisjunction { s =>
if (s.length <= 140) Tweet(s).right
else (new FailureReason { def description: String = "Too long to be a tweet!" }).left
}