From 8e0817aadc2b454ef056b9c6e08fbeea63d03805 Mon Sep 17 00:00:00 2001 From: jrudnick Date: Mon, 2 Oct 2017 13:46:34 -0400 Subject: [PATCH] Adding support for nonEmptyList and nonEmptyVector (cats) based config parsers --- README.md | 2 +- build.sbt | 1 + .../ccadllc/cedi/config/ConfigParser.scala | 49 ++++++++++++++++--- .../cedi/config/ConfigParserTest.scala | 21 ++++++++ readme/src/main/tut/README.md | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9af8579..9c8b171 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ objects to application specific types. The differentiating features of this libr - errors in the configuration are returned as values -- exceptions are never thrown - all errors present in the configuration are reported, not just the first error that is encountered - configuration parsers can be built manually or derived automatically from the structure of application specific types - - limited dependencies -- only Typesafe Config and Shapeless + - limited dependencies -- only Typesafe Config, Shapeless, and Cats Example of usage: diff --git a/build.sbt b/build.sbt index 32e6621..69b14e5 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,7 @@ lazy val core = project.in(file("core")).enablePlugins(SbtOsgi). libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.3.0", "com.chuusai" %% "shapeless" % "2.3.2", + "org.typelevel" %% "cats-core" % "1.0.0-MF", "org.scalatest" %% "scalatest" % "3.0.1" % "test" ), buildOsgiBundle("com.ccadllc.cedi.config") diff --git a/core/src/main/scala/com/ccadllc/cedi/config/ConfigParser.scala b/core/src/main/scala/com/ccadllc/cedi/config/ConfigParser.scala index dfc6942..77aca51 100644 --- a/core/src/main/scala/com/ccadllc/cedi/config/ConfigParser.scala +++ b/core/src/main/scala/com/ccadllc/cedi/config/ConfigParser.scala @@ -19,7 +19,8 @@ import scala.collection.JavaConverters._ import scala.concurrent.duration.FiniteDuration import com.typesafe.config.{ Config, ConfigException, ConfigFactory, ConfigMemorySize, ConfigValue } -import shapeless.{ ::, HList, HNil, Generic, LabelledGeneric, Lazy, Typeable } +import cats.data._ +import shapeless.{ :: => #:, HList, HNil, Generic, LabelledGeneric, Lazy, Typeable } /** * Parses a value of type `A` from a Typesafe `Config` object. @@ -334,11 +335,11 @@ object ConfigParser { ConfigParser("failed") { _ => Left(ConfigErrors.of(error)) } /** - * Creates a parser that ignores the `Config` object and always returns the supplied error list. - * - * @param errors the errors to be returned - * @return a failed parser - */ + * Creates a parser that ignores the `Config` object and always returns the supplied error list. + * + * @param errors the errors to be returned + * @return a failed parser + */ def failed(errors: ConfigErrors): ConfigParser[Nothing] = ConfigParser("failed") { _ => Left(errors) } @@ -498,6 +499,22 @@ object ConfigParser { } } + /** + * Like [[list]] but ensures a `NonEmptyList`. + * + * Warning: errors may not be collected for the full list + * + * @param key config key of the config list to parse + * @param cpl provides a config parser to use to parse each config element of the config list, for a given key + * @return new parser + */ + def nonEmptyList[A](key: String)(cpl: String => ConfigParser[List[A]]): ConfigParser[NonEmptyList[A]] = cpl(key) bind { + case head :: tail => ConfigParser.pure(NonEmptyList(head, tail)) + case Nil => ConfigParser(s"non-empty-list failure for $key") { _ => + Left(ConfigErrors.of(ConfigError.WrongType(ConfigKey.Relative(key), "expected non-empty list", None))) + } + } + /** * Like [[list]] but returns results as a `Vector` instead of a `List`. * @@ -507,6 +524,22 @@ object ConfigParser { */ def vector[A](key: String)(cp: ConfigParser[A]): ConfigParser[Vector[A]] = list(key)(cp).map(_.toVector) + /** + * Like [[vector]] but ensures a `NonEmptyVector`. + * + * Warning: errors may not be collected for the full vector + * + * @param key config key of the config vector to parse + * @param cpl provides a config parser to use to parse each config element of the config vector, for a given key + * @return new parser + */ + def nonEmptyVector[A](key: String)(cpv: String => ConfigParser[Vector[A]]): ConfigParser[NonEmptyVector[A]] = cpv(key) bind { + case Vector() => ConfigParser(s"non-empty-vector failure for $key") { _ => + Left(ConfigErrors.of(ConfigError.WrongType(ConfigKey.Relative(key), "expected non-empty vector", None))) + } + case v => ConfigParser.pure(NonEmptyVector(v.head, v.tail)) + } + private def doGet[A](key: String, tpe: String)(get: Config => A): ConfigParser[A] = { val k = ConfigKey.Relative(key) ConfigParser(s"""$tpe("$key")""") { cfg => @@ -588,7 +621,7 @@ object ConfigParser { * @param that element parser to extend with * @return new parser */ - def ::[A](that: ConfigParser[A]): ConfigParser[A :: L] = that.map2(self) { (a, l) => a :: l }.withToString(s"($that :: $self)") + def ::[A](that: ConfigParser[A]): ConfigParser[A #: L] = that.map2(self) { (a, l) => a :: l }.withToString(s"($that :: $self)") } /** Supports `::` syntax. */ @@ -601,7 +634,7 @@ object ConfigParser { * @param that element parser to extend with * @return new parser */ - def ::[B](that: ConfigParser[B]): ConfigParser[B :: A :: HNil] = that :: self.mapPreservingToString(_ :: HNil) + def ::[B](that: ConfigParser[B]): ConfigParser[B #: A #: HNil] = that :: self.mapPreservingToString(_ :: HNil) } /** Supports `as` syntax. */ diff --git a/core/src/test/scala/com/ccadllc/cedi/config/ConfigParserTest.scala b/core/src/test/scala/com/ccadllc/cedi/config/ConfigParserTest.scala index 618664a..78ade91 100644 --- a/core/src/test/scala/com/ccadllc/cedi/config/ConfigParserTest.scala +++ b/core/src/test/scala/com/ccadllc/cedi/config/ConfigParserTest.scala @@ -19,6 +19,7 @@ import scala.concurrent.duration._ import com.typesafe.config.{ Config, ConfigFactory, ConfigOriginFactory } import org.scalatest.{ Matchers, WordSpec } +import cats.data._ case class AddressAndPort(address: String, port: Int) object AddressAndPort { @@ -87,6 +88,26 @@ class ConfigParserTest extends WordSpec with Matchers { )) } + "support non-empty list" in { + val loi = { cpl: String => ConfigParser.intList(cpl) } + + ConfigParser.nonEmptyList[Int]("counts")(loi).parse(ConfigFactory.parseString("counts: [1, 2, 3, 4, 5]")) shouldBe Right(NonEmptyList(1, List(2, 3, 4, 5))) + + ConfigParser.nonEmptyList[Int]("counts")(loi).parse(ConfigFactory.parseString("counts: [1]")) shouldBe Right(NonEmptyList(1, List.empty)) + + ConfigParser.nonEmptyList[Int]("counts")(loi).parse(ConfigFactory.parseString("counts: []")) shouldBe Left(ConfigErrors.of(ConfigError.WrongType(ConfigKey.Relative("counts"), "expected non-empty list", None))) + } + + "support non-empty vector" in { + val voi = { cpl: String => ConfigParser.intList(cpl).map { _.toVector } } + + ConfigParser.nonEmptyVector[Int]("counts")(voi).parse(ConfigFactory.parseString("counts: [1, 2, 3, 4, 5]")) shouldBe Right(NonEmptyVector(1, Vector(2, 3, 4, 5))) + + ConfigParser.nonEmptyVector[Int]("counts")(voi).parse(ConfigFactory.parseString("counts: [1]")) shouldBe Right(NonEmptyVector(1, Vector.empty)) + + ConfigParser.nonEmptyVector[Int]("counts")(voi).parse(ConfigFactory.parseString("counts: []")) shouldBe Left(ConfigErrors.of(ConfigError.WrongType(ConfigKey.Relative("counts"), "expected non-empty vector", None))) + } + "provides a humanized summary of config errors" in { val errors = ConfigErrors.of( ConfigError.WrongType(ConfigKey.Relative("connection.server"), "invalid syntax: must be address:port", testOrigin), diff --git a/readme/src/main/tut/README.md b/readme/src/main/tut/README.md index dd92a38..a113d28 100644 --- a/readme/src/main/tut/README.md +++ b/readme/src/main/tut/README.md @@ -14,7 +14,7 @@ objects to application specific types. The differentiating features of this libr - errors in the configuration are returned as values -- exceptions are never thrown - all errors present in the configuration are reported, not just the first error that is encountered - configuration parsers can be built manually or derived automatically from the structure of application specific types - - limited dependencies -- only Typesafe Config and Shapeless + - limited dependencies -- only Typesafe Config, Shapeless, and Cats Example of usage: