Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for nonEmptyList and nonEmptyVector #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
49 changes: 41 additions & 8 deletions core/src/main/scala/com/ccadllc/cedi/config/ConfigParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) }

Expand Down Expand Up @@ -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`.
*
Expand All @@ -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 =>
Expand Down Expand Up @@ -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. */
Expand All @@ -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. */
Expand Down
21 changes: 21 additions & 0 deletions core/src/test/scala/com/ccadllc/cedi/config/ConfigParserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion readme/src/main/tut/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down