diff --git a/README.md b/README.md index cc1bed9..76386d0 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,15 @@ val boolPath: Configz[Boolean] = "some.path.to.a.bool".path[Boolean] val intPath: Configz[Int] = "some.path.to.an.int".path[Int] // Get the values at the path, which may fail with a com.typesafe.config.ConfigException. -val boolProp: ValidationNEL[ConfigException, Boolean] = config.get(boolPath) -val intProp: ValidationNEL[ConfigException, Int] = config.get(intPath) +val boolProp: Settings[Boolean] = config.get(boolPath) +val intProp: Settings[Boolean, Int] = config.get(intPath) -// Configz is an applicative functor, so you can combine them (using scalaz operators): -val boolIntConfig: Configz[(Boolean, Int)] = (boolPath |@| intPath)(_ -> _) -val boolIntProp: ValidationNEL[ConfigException, (Boolean, Int)] = config.get(boolIntConfig) +// Note that Settings[A] is an alias for ValidationNEL[ConfigException, A]. + +// Configz is an applicative functor, so you can combine them (using scalaz operators like <*> or |@|): +val boolIntConfig: Configz[(Boolean, Int)] = (boolPath |@| intPath)(_ -> _) +val boolIntProp: Settings[(Boolean, Int)] = config.get(boolIntConfig) + +// Configz paths can have custom validation using the >=> (Kleisli) operator: +val validatedIntPath = intPath >=> validate((_: Int) > 1000, "some.path.to.an.int must be > 1000") ``` \ No newline at end of file diff --git a/core/src/main/scala/Configz.scala b/core/src/main/scala/Configz.scala index a4650a1..fce354f 100644 --- a/core/src/main/scala/Configz.scala +++ b/core/src/main/scala/Configz.scala @@ -7,10 +7,12 @@ import Scalaz._ /** Reads settings from a [[com.typesafe.config.Config]]. */ sealed trait Configz[A] { /** Read the settings from a config. */ - def settings(config: Config): ValidationNEL[ConfigException, A] + def settings(config: Config): Settings[A] } object Configz { + implicit def configzToKleisli[A](configz: Configz[A]): Kleisli[Settings, Config, A] = kleisli(configz.settings) + implicit val ConfigzPure: Pure[Configz] = new Pure[Configz] { def pure[A](a: => A): Configz[A] = new Configz[A] { def settings(config: Config) = diff --git a/core/src/main/scala/package.scala b/core/src/main/scala/package.scala index 93847c4..c20a5ce 100644 --- a/core/src/main/scala/package.scala +++ b/core/src/main/scala/package.scala @@ -19,21 +19,36 @@ package net.rosien * val boolProp: Settings[Boolean] = config.get(boolPath) * val intProp: Settings[Int] = config.get(intPath) * - * // Configz is an applicative functor, so you can combine them: + * // Configz is an applicative functor, so you can combine them (using scalaz operators like <*> or |@|): * val boolIntConfig: Configz[(Boolean, Int)] = (boolPath |@| intPath)(_ -> _) - * val boolIntProp: Settings[(Boolean, Int)] = config.get(boolIntProp) + * val boolIntProp: Settings[(Boolean, Int)] = config.get(boolIntConfig) + * + * // Configz paths can have custom validation using the >=> (Kleisli) operator: + * val validatedIntPath = intPath >=> validate((_: Int) > 1000, "some.path.to.an.int must be > 1000") * }}} */ package object configz { import com.typesafe.config._ import scalaz._ import Scalaz._ + import Validation.Monad._ import Configz._ + type Settings[A] = ValidationNEL[ConfigException, A] + + implicit val SettingsBind = implicitly[Bind[Settings]] + + /** Validate a Configz path. + * @param f predicate function + * @param message failure message if f returns false + * @return validation function to be composed with Configz via >=> operator + */ + def validate[A](f: A => Boolean, message: String): A => Settings[A] = prop => + if (f(prop)) prop.successNel else new ConfigException.Generic(message).failNel[A] /** Additional methods on [[com.typesafe.config.Config]]. */ class ConfigOps(config: Config) { - def get[A](configz: Configz[A]): ValidationNEL[ConfigException, A] = configz.settings(config) + def get[A](configz: Kleisli[Settings, Config, A]): ValidationNEL[ConfigException, A] = configz(config) } implicit def configToConfigOps(config: Config): ConfigOps = new ConfigOps(config) diff --git a/core/src/test/scala/ConfigzSpec.scala b/core/src/test/scala/ConfigzSpec.scala index af52cb6..c3d8142 100644 --- a/core/src/test/scala/ConfigzSpec.scala +++ b/core/src/test/scala/ConfigzSpec.scala @@ -5,8 +5,9 @@ import org.specs2.scalaz.ValidationMatchers class ConfigzSpec extends Specification with ValidationMatchers { def is = "Configz should" ^ - "accumulate errors" ! configz().errors ^ - "read settings" ! configz().settings ^ + "accumulate errors" ! configz().errors ^ + "read settings" ! configz().settings ^ + "validate via kleisli" ! configz().validateKleisli ^ end case class configz() { @@ -16,6 +17,8 @@ class ConfigzSpec extends Specification with ValidationMatchers { import Scalaz._ val config = ConfigFactory.load + val boolProp = "configz.bool".path[Boolean] + val intProp = "configz.int".path[Int] def errors = { val missing = "configz.asdf".path[String] @@ -28,12 +31,12 @@ class ConfigzSpec extends Specification with ValidationMatchers { } } - def settings = { - val b = "configz.bool".path[Boolean] - val i = "configz.int".path[Int] - val bi = b <|*|> i + def settings = config.get(boolProp <|*|> intProp) must beSuccessful(true -> 1234) - config.get(bi) must beSuccessful(true -> 1234) + def validateKleisli = { + val validIntProp = validate((_: Int) < 1000, "configz.int must be < 1000") + + (config.get(intProp >=> validIntProp) must beFailing) } } } \ No newline at end of file