Skip to content

Commit

Permalink
Merge pull request #9 from vlovgr/generic
Browse files Browse the repository at this point in the history
Add ciris-generic module using shapeless
  • Loading branch information
vlovgr authored May 14, 2017
2 parents e4802e2 + 96a9b47 commit 2c2feb0
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 15 deletions.
33 changes: 28 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ lazy val ciris = project
.aggregate(
coreJS, coreJVM,
enumeratumJS, enumeratumJVM,
genericJS, genericJVM,
refinedJS, refinedJVM,
squantsJS, squantsJVM
)
Expand All @@ -38,6 +39,24 @@ lazy val enumeratum = crossProject
lazy val enumeratumJS = enumeratum.js
lazy val enumeratumJVM = enumeratum.jvm

lazy val generic = crossProject
.in(file("modules/generic"))
.settings(moduleName := "ciris-generic", name := "Ciris generic")
.settings(
libraryDependencies ++=
Seq(
"com.chuusai" %%% "shapeless" % "2.3.2",
compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" % Test cross CrossVersion.patch)
)
)
.settings(scalaSettings)
.settings(releaseSettings)
.settings(testSettings)
.dependsOn(core % "compile;test->test")

lazy val genericJS = generic.js
lazy val genericJVM = generic.jvm

lazy val refined = crossProject
.in(file("modules/refined"))
.settings(moduleName := "ciris-refined", name := "Ciris refined")
Expand Down Expand Up @@ -87,11 +106,12 @@ lazy val docs = project
crossScalaVersions,
BuildInfoKey.map(moduleName in coreJVM) { case (k, v) "core" + k.capitalize -> v },
BuildInfoKey.map(moduleName in enumeratumJVM) { case (k, v) "enumeratum" + k.capitalize -> v },
BuildInfoKey.map(moduleName in genericJVM) { case (k, v) "generic" + k.capitalize -> v },
BuildInfoKey.map(moduleName in refinedJVM) { case (k, v) "refined" + k.capitalize -> v },
BuildInfoKey.map(moduleName in squantsJVM) { case (k, v) "squants" + k.capitalize -> v }
)
)
.dependsOn(coreJVM, enumeratumJVM, refinedJVM, squantsJVM)
.dependsOn(coreJVM, enumeratumJVM, genericJVM, refinedJVM, squantsJVM)
.enablePlugins(BuildInfoPlugin, MicrositesPlugin)

lazy val scala210 = "2.10.6"
Expand All @@ -118,9 +138,11 @@ lazy val scalaSettings = Seq(
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xfuture",
"-Ywarn-unused-import"
"-Ywarn-unused-import",
"-Ywarn-unused"
).filter {
case "-Ywarn-unused-import" if scalaVersion.value == scala210 false
case "-Ywarn-unused" if scalaVersion.value != scala212 false
case _ true
},
scalacOptions in (Compile, console) -= "-Ywarn-unused-import",
Expand Down Expand Up @@ -254,12 +276,13 @@ generateScripts in ThisBuild := {
|)
|
|~/.coursier/coursier launch -q -P \\
| com.lihaoyi:ammonite_2.12.2:0.8.4 \\
| com.lihaoyi:ammonite_2.12.2:0.8.5 \\
| $organizationId:${(moduleName in coreJVM).value}_2.12:$moduleVersion \\
| $organizationId:${(moduleName in enumeratumJVM).value}_2.12:$moduleVersion \\
| $organizationId:${(moduleName in genericJVM).value}_2.12:$moduleVersion \\
| $organizationId:${(moduleName in refinedJVM).value}_2.12:$moduleVersion \\
| $organizationId:${(moduleName in squantsJVM).value}_2.12:$moduleVersion \\
| -- --predef 'import ciris._,ciris.enumeratum._,ciris.refined._,ciris.squants._' < /dev/tty
| -- --predef 'import ciris._,ciris.enumeratum._,ciris.generic._,ciris.refined._,ciris.squants._' < /dev/tty
""".stripMargin.trim + "\n"

IO.createDirectory(output)
Expand All @@ -275,7 +298,7 @@ updateScripts in ThisBuild := {
}
}

lazy val allModules = List("core", "enumeratum", "refined", "squants")
lazy val allModules = List("core", "enumeratum", "generic", "refined", "squants")
lazy val allModulesJS = allModules.map(_ + "JS")
lazy val allModulesJVM = allModules.map(_ + "JVM")

Expand Down
10 changes: 10 additions & 0 deletions docs/src/main/tut/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ s"""
|libraryDependencies ++= Seq(
| "$organization" %% "$coreModuleName" % "$latestVersion",
| "$organization" %% "$enumeratumModuleName" % "$latestVersion",
| "$organization" %% "$genericModuleName" % "$latestVersion",
| "$organization" %% "$refinedModuleName" % "$latestVersion",
| "$organization" %% "$squantsModuleName" % "$latestVersion"
|)
Expand All @@ -46,9 +47,16 @@ and make sure to replace `%%` with `%%%` if you are using Scala.js.
The only required module is `ciris-core`, the rest are optional library integrations.

- The `ciris-enumeratum` module allows loading [enumeratum][enumeratum] enumerations.
- The `ciris-generic` module allows loading more types with [shapeless][shapeless].
- The `ciris-refined` module allows loading [refined][refined] refinement types.
- The `ciris-squants` module allows loading [squants][squants] data types.

If you're using `ciris-generic` with Scala 2.10, you'll need to include the [Macro Paradise](http://docs.scala-lang.org/overviews/macros/paradise.html) compiler plugin.

```
libraryDependencies += compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
```

#### Ammonite
To start an [Ammonite REPL](http://www.lihaoyi.com/Ammonite/#Ammonite-REPL) with Ciris loaded and imported, simply run the following.
```
Expand All @@ -65,6 +73,7 @@ println(
s"""
|import $$ivy.`$organization::$coreModuleName:$latestVersion`, ciris._
|import $$ivy.`$organization::$enumeratumModuleName:$latestVersion`, ciris.enumeratum._
|import $$ivy.`$organization::$genericModuleName:$latestVersion`, ciris.generic._
|import $$ivy.`$organization::$refinedModuleName:$latestVersion`, ciris.refined._
|import $$ivy.`$organization::$squantsModuleName:$latestVersion`, ciris.squants._
""".stripMargin.trim
Expand Down Expand Up @@ -211,6 +220,7 @@ withValue(env[Option[AppEnvironment]]("APP_ENV")) {

[enumeratum]: https://github.com/lloydmeta/enumeratum
[refined]: https://github.com/fthomas/refined
[shapeless]: https://github.com/milessabin/shapeless
[squants]: http://www.squants.com
[sbt]: http://www.scala-sbt.org
[scala]: http://www.scala-lang.org
Expand Down
18 changes: 18 additions & 0 deletions modules/core/shared/src/main/scala/ciris/ConfigError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@ package ciris
sealed abstract class ConfigError {
def message: String

final def combine(error: ConfigError): ConfigError =
ConfigError.combined(this, error)

final def append(error: ConfigError): ConfigErrors =
ConfigErrors(this) append error
}

object ConfigError {
def apply(message: String): ConfigError = {
val theMessage = message
new ConfigError {
override val message: String = theMessage
override def toString: String = s"ConfigError($message)"
}
}

sealed abstract case class Combined(errors: Vector[ConfigError]) extends ConfigError {
override def message: String = errors.map(_.message).mkString(", ")
}

def combined(error1: ConfigError, error2: ConfigError, rest: ConfigError*): Combined =
new Combined(Vector(error1, error2) ++ rest) {}

final case class MissingKey(key: String, keyType: ConfigKeyType) extends ConfigError {
override def message: String = s"Missing ${keyType.value} [$key]"
}
Expand Down
5 changes: 5 additions & 0 deletions modules/core/shared/src/main/scala/ciris/ConfigReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import scala.util.{Failure, Success, Try}
sealed abstract class ConfigReader[A] { self
def read(key: String)(implicit source: ConfigSource): Either[ConfigError, A]

final def map[B](f: A B): ConfigReader[B] =
ConfigReader.pure { (key, source)
self.read(key)(source).fold(Left.apply, value Right(f(value)))
}

final def mapOption[B](typeName: String)(f: A Option[B]): ConfigReader[B] =
ConfigReader.pure { (key, source)
self
Expand Down
13 changes: 10 additions & 3 deletions modules/core/shared/src/test/scala/ciris/ConfigErrorsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ final class ConfigErrorsSpec extends PropertySpec {
"ConfigErrors" when {
"converting to String" should {
"list the errors" in {
val configErrors = ConfigErrors(MissingKey("key", ConfigKeyType.Environment))
configErrors.toString shouldBe "ConfigErrors(MissingKey(key,Environment))"
val configErrors =
ConfigErrors(MissingKey("key", ConfigKeyType.Environment))
.append(ConfigError("a"))

configErrors.toString shouldBe "ConfigErrors(MissingKey(key,Environment),ConfigError(a))"
}
}

Expand All @@ -17,11 +20,15 @@ final class ConfigErrorsSpec extends PropertySpec {
ConfigErrors(MissingKey("key", ConfigKeyType.Environment))
.append(ReadException("key2", ConfigKeyType.Properties, new Error("error")))
.append(WrongType("key3", "value3", "Int", ConfigKeyType.Environment, cause = None))
.append(ConfigError("a"))
.append(ConfigError.combined(ConfigError("b"), ConfigError("c")))

configErrors.messages shouldBe Vector(
"Missing environment variable [key]",
"Exception while reading system property [key2]: java.lang.Error: error",
"Environment variable [key3] with value [value3] cannot be converted to type [Int]"
"Environment variable [key3] with value [value3] cannot be converted to type [Int]",
"a",
"b, c"
)
}
}
Expand Down
3 changes: 3 additions & 0 deletions modules/core/shared/src/test/scala/ciris/PropertySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import org.scalatest.{Matchers, WordSpec}
import scala.util.Try

class PropertySpec extends WordSpec with Matchers with PropertyChecks {
override implicit val generatorDrivenConfig: PropertyCheckConfiguration =
PropertyCheckConfiguration(minSuccessful = 1000)

def mixedCase(string: String): Gen[String] = {
(for {
lowers Gen.listOfN(string.length, Gen.oneOf(true, false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,16 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {
}

"return a failure for other values" in {
forAll { byte: Byte
whenever(ByteEnumItem.withValueOpt(byte).isEmpty) {
readValue[ByteEnumItem](byte.toString) shouldBe a[Left[_, _]]
}
}
}

"return a failure for wrong type values" in {
forAll { string: String
whenever(!ByteEnumItem.values.map(_.value.toString).contains(string)) {
whenever(fails(string.toByte)) {
readValue[ByteEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -109,8 +117,16 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {
}

"return a failure for other values" in {
forAll { char: Char
whenever(CharEnumItem.withValueOpt(char).isEmpty) {
readValue[CharEnumItem](char.toString) shouldBe a[Left[_, _]]
}
}
}

"return a failure for wrong type values" in {
forAll { string: String
whenever(!CharEnumItem.values.map(_.value.toString).contains(string)) {
whenever(string.length != 1) {
readValue[CharEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -132,7 +148,7 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {

"return a failure for other values" in {
forAll { string: String
whenever(!EnumEntryItem.values.map(_.entryName).contains(string)) {
whenever(EnumEntryItem.withNameOption(string).isEmpty) {
readValue[EnumEntryItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -153,8 +169,16 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {
}

"return a failure for other values" in {
forAll { int: Int
whenever(IntEnumItem.withValueOpt(int).isEmpty) {
readValue[IntEnumItem](int.toString) shouldBe a[Left[_, _]]
}
}
}

"return a failure for wrong type values" in {
forAll { string: String
whenever(!IntEnumItem.values.map(_.value.toString).contains(string)) {
whenever(fails(string.toInt)) {
readValue[IntEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -175,8 +199,16 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {
}

"return a failure for other values" in {
forAll { long: Long
whenever(LongEnumItem.withValueOpt(long).isEmpty) {
readValue[LongEnumItem](long.toString) shouldBe a[Left[_, _]]
}
}
}

"return a failure for wrong type values" in {
forAll { string: String
whenever(!LongEnumItem.values.map(_.value.toString).contains(string)) {
whenever(fails(string.toLong)) {
readValue[LongEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -197,8 +229,16 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {
}

"return a failure for other values" in {
forAll { short: Short
whenever(ShortEnumItem.withValueOpt(short).isEmpty) {
readValue[ShortEnumItem](short.toString) shouldBe a[Left[_, _]]
}
}
}

"return a failure for wrong type values" in {
forAll { string: String
whenever(!ShortEnumItem.values.map(_.value.toString).contains(string)) {
whenever(fails(string.toShort)) {
readValue[ShortEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand All @@ -220,7 +260,7 @@ final class EnumeratumConfigReadersSpec extends PropertySpec {

"return a failure for other values" in {
forAll { string: String
whenever(!StringEnumItem.values.map(_.value.toString).contains(string)) {
whenever(StringEnumItem.withValueOpt(string).isEmpty) {
readValue[StringEnumItem](string) shouldBe a[Left[_, _]]
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ciris

import ciris.generic.readers.GenericConfigReaders

package object generic extends GenericConfigReaders
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ciris.generic.readers

import ciris.{ConfigError, ConfigReader}
import shapeless.{:+:, ::, CNil, Coproduct, Generic, HNil, Inl, Inr, Lazy}

trait GenericConfigReaders {
implicit val cNilConfigReader: ConfigReader[CNil] =
ConfigReader.pure { (key, source)
Left(ConfigError(
s"Could not find any valid coproduct choice while reading ${source.keyType.value} [$key]"))
}

implicit def coproductConfigReader[A, B <: Coproduct](
implicit readA: Lazy[ConfigReader[A]],
readB: ConfigReader[B]
): ConfigReader[A :+: B] = {
ConfigReader.pure { (key, source)
readA.value.read(key)(source) match {
case Right(a) Right(Inl(a))
case Left(aError)
readB.read(key)(source) match {
case Right(b) Right(Inr(b))
case Left(bError) Left(aError combine bError)
}
}
}
}

implicit def hListArityOneConfigReader[A](
implicit readA: Lazy[ConfigReader[A]]
): ConfigReader[A :: HNil] = {
readA.value.map(_ :: HNil)
}

implicit def genericConfigReader[A, B](
implicit gen: Generic.Aux[A, B],
readB: Lazy[ConfigReader[B]]
): ConfigReader[A] = {
readB.value.map(gen.from)
}
}
Loading

0 comments on commit 2c2feb0

Please sign in to comment.