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

Feature/multiple naming strategies #45

Merged
Merged
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
24 changes: 14 additions & 10 deletions core/src/main/scala/configs/ConfigKeyNaming.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ package configs
import configs.ConfigUtil.splitWords
import java.util.Locale

trait ConfigKeyNaming[A] {
trait ConfigKeyNaming[A] { self =>

def apply(field: String): String
def apply(field: String): Seq[String]

def andThen(f: String => String): ConfigKeyNaming[A] =
field => f(apply(field))
def applyFirst(field: String): String = apply(field).head

def andThen(f: String => Seq[String]): ConfigKeyNaming[A] =
field => self.apply(field).flatMap(f)

def or(f: String => Seq[String]): ConfigKeyNaming[A] =
field => self.apply(field) ++ f(field)
}

object ConfigKeyNaming {
Expand All @@ -38,37 +42,37 @@ object ConfigKeyNaming {
_identity.asInstanceOf[ConfigKeyNaming[A]]

private[this] val _identity: ConfigKeyNaming[Any] =
x => x
x => Seq(x)


def hyphenSeparated[A]: ConfigKeyNaming[A] =
_hyphenSeparated.asInstanceOf[ConfigKeyNaming[A]]

private[this] val _hyphenSeparated: ConfigKeyNaming[Any] =
splitWords(_).mkString("-").toLowerCase(Locale.ROOT)
x => Seq(splitWords(x).mkString("-").toLowerCase(Locale.ROOT))


def snakeCase[A]: ConfigKeyNaming[A] =
_snakeCase.asInstanceOf[ConfigKeyNaming[A]]

private[this] val _snakeCase: ConfigKeyNaming[Any] =
splitWords(_).mkString("_").toLowerCase(Locale.ROOT)
x => Seq(splitWords(x).mkString("_").toLowerCase(Locale.ROOT))


def lowerCamelCase[A]: ConfigKeyNaming[A] =
_lowerCamelCase.asInstanceOf[ConfigKeyNaming[A]]

private[this] val _lowerCamelCase: ConfigKeyNaming[Any] =
splitWords(_) match {
x => Seq(splitWords(x) match {
case Nil => ""
case h :: t => (h.toLowerCase(Locale.ROOT) :: t.map(_.capitalize)).mkString
}
})


def upperCamelCase[A]: ConfigKeyNaming[A] =
_upperCamelCase.asInstanceOf[ConfigKeyNaming[A]]

private[this] val _upperCamelCase: ConfigKeyNaming[Any] =
splitWords(_).map(_.capitalize).mkString
x => Seq(splitWords(x).map(_.capitalize).mkString)

}
12 changes: 11 additions & 1 deletion core/src/main/scala/configs/ConfigReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package configs
import com.typesafe.config.ConfigException
import java.util.concurrent.TimeUnit
import java.{lang => jl, math => jm, time => jt, util => ju}

import scala.annotation.compileTimeOnly
import scala.collection.compat._
import scala.concurrent.duration.{Duration, FiniteDuration}
Expand All @@ -28,8 +29,17 @@ trait ConfigReader[A] {

protected def get(config: Config, path: String): Result[A]

final def read(config: Config, path: String): Result[A] =
final def read(config: Config, path: String): Result[A] = {
get(config, path).pushPath(path)
}

final def read(config: Config, path: Seq[String]): Result[A] = {
// take first success, if all failed the last failure
path.zipWithIndex.iterator
.map { case (p,i) => (read(config, p),i)}
.find { case (p,i) => p.isSuccess || i == path.size - 1 }
.get._1
}

final def extract(config: Config): Result[A] = {
val p = "extract"
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/configs/macros/ConfigReaderMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ private[macros] trait ConfigReaderMacroImpl {

class DerivingReaderContext(val naming: Tree, val cache: ConfigReaderCache) extends DerivingContext {
type Cache = ConfigReaderCache

// for reading we take the all names produced by ConfigKeyNaming
def configKey(field: String): Tree = q"$n.apply($field)"
}

class ConfigReaderCache extends InstanceCache {
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/configs/macros/ConfigWriterMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ private[macros] trait ConfigWriterMacroImpl {

class DerivingWriterContext(val naming: Tree, val cache: ConfigWriterCache) extends DerivingContext {
type Cache = ConfigWriterCache

// for writing we take the first name produced by ConfigKeyNaming
def configKey(field: String): Tree = q"$n.applyFirst($field)"
}

class ConfigWriterCache extends InstanceCache {
Expand Down
10 changes: 4 additions & 6 deletions core/src/main/scala/configs/macros/Construct.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,11 @@ trait Construct {
val params =
tpe.decls.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor =>
if (m.paramLists.lengthCompare(1) <= 0) {
val ds = defaults(a, m)
m.paramLists.flatten.zipWithIndex.map {
case (s, i) => Param(s, ds.get(i))
}
val ds = defaults(a, m)
// only use first parameter list of case class constructor
m.paramLists.head.zipWithIndex.map {
case (s, i) => Param(s, ds.get(i))
}
else abort(a, "primary constructor has multi param list")
}.getOrElse(abort(a, "bug?"))
val accessors =
tpe.decls.sorted.collect {
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/configs/macros/MacroBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ private[macros] abstract class MacroBase {

def naming: Tree

private[this] val n = freshName("n")
protected val n = freshName("n")

def configKey(field: String): Tree = q"$n($field)"
def configKey(field: String): Tree

def valDefs: List[Tree] = {
val nv = q"val $n = $naming"
Expand Down
58 changes: 14 additions & 44 deletions core/src/test/scala/configs/ConfigKeyNamingTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

package configs

import configs.testutil.fun._
import scalaprops.Property.forAll
import scalaprops.{Gen, Scalaprops}
import scalaprops.ScalapropsScalaz._
import scalaz.syntax.apply._
import collection.JavaConverters._

object ConfigKeyNamingTest extends Scalaprops {

Expand All @@ -36,84 +36,54 @@ object ConfigKeyNamingTest extends Scalaprops {
implicit val naming: ConfigKeyNaming[User] = ConfigKeyNaming.identity

forAll { user: User =>
val expected =
s"""emailAddress=${user.emailAddress}
|name=${user.name}
|siteURL=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("emailAddress", "name", "siteURL")
}
}

val hyphenSeparated = {
implicit val naming: ConfigKeyNaming[User] = ConfigKeyNaming.hyphenSeparated

forAll { user: User =>
val expected =
s"""email-address=${user.emailAddress}
|name=${user.name}
|site-url=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("email-address", "name", "site-url")
}
}

val snakeCase = {
implicit val naming: ConfigKeyNaming[User] = ConfigKeyNaming.snakeCase

forAll { user: User =>
val expected =
s""""email_address"=${user.emailAddress}
|name=${user.name}
|"site_url"=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("email_address", "name", "site_url")
}
}

val lowerCamelCase = {
implicit val naming: ConfigKeyNaming[User] = ConfigKeyNaming.lowerCamelCase

forAll { user: User =>
val expected =
s"""emailAddress=${user.emailAddress}
|name=${user.name}
|siteURL=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("emailAddress", "name", "siteURL")
}
}

val upperCamelCase = {
implicit val naming: ConfigKeyNaming[User] = ConfigKeyNaming.upperCamelCase

forAll { user: User =>
val expected =
s"""EmailAddress=${user.emailAddress}
|Name=${user.name}
|SiteURL=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("EmailAddress", "Name", "SiteURL")
}
}

val andThen = {
implicit val naming: ConfigKeyNaming[User] =
ConfigKeyNaming.hyphenSeparated.andThen("user-" + _)
ConfigKeyNaming.hyphenSeparated.andThen(x => Seq("user-" + x))

forAll { user: User =>
val expected =
s"""user-email-address=${user.emailAddress}
|user-name=${user.name}
|user-site-url=${user.siteURL}
|""".stripMargin

render(user) == expected
val result = ConfigWriter[User].write(user).asInstanceOf[ConfigObject]
result.keySet().asScala == Set("user-email-address", "user-name", "user-site-url")
}
}

Expand Down
26 changes: 8 additions & 18 deletions core/src/test/scala/configs/DeriveForBeanTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import configs.testutil.instance.anyVal._
import configs.testutil.instance.tuple._
import configs.testutil.{Bean1, Bean22, Bean484}
import java.util.Objects

import scala.beans.{BeanProperty, BooleanBeanProperty}
import scalaprops.Property.forAll
import scalaprops.{Gen, Lazy, Properties, Scalaprops}
import scalaprops.ScalapropsScalaz._
import scalaz.syntax.apply._
import scalaz.syntax.equal._
import scalaz.Equal
import collection.JavaConverters._

object DeriveForBeanTest extends Scalaprops {

Expand Down Expand Up @@ -143,13 +145,8 @@ object DeriveForBeanTest extends Scalaprops {
implicit val naming: ConfigKeyNaming[Foo] = ConfigKeyNaming.identity

forAll { foo: Foo =>
val expected =
s"""URL=${foo.getURL}
|fooBah=${foo.getFooBah}
|x=${foo.getX}
|""".stripMargin

render(foo) == expected
val result = ConfigWriter[Foo].write(foo).asInstanceOf[ConfigObject]
result.unwrapped.asScala.toSet == Set("URL" -> foo.getURL, "fooBah" -> foo.getFooBah, "x" -> foo.getX)
}
}

Expand All @@ -165,12 +162,8 @@ object DeriveForBeanTest extends Scalaprops {
(Gen[Boolean] |@| Gen[Boolean])(new BooleanProps(_, _))

forAll { bool: BooleanProps =>
val expected =
s"""primitive=${bool.isPrimitive}
|wrapped=${bool.isWrapped}
|""".stripMargin

render(bool) == expected
val result = ConfigWriter[BooleanProps].write(bool).asInstanceOf[ConfigObject]
result.unwrapped.asScala.toSet == Set("primitive" -> bool.isPrimitive, "wrapped" -> bool.isWrapped)
}
}

Expand All @@ -183,11 +176,8 @@ object DeriveForBeanTest extends Scalaprops {
}

forAll {
val expected =
s"""boolean=true
|""".stripMargin

render(new Foo()) == expected
val result = ConfigWriter[Foo].write(new Foo()).asInstanceOf[ConfigObject]
result.unwrapped.asScala.toSet == Set("boolean" -> true.asInstanceOf[AnyRef])
}
}

Expand Down
16 changes: 8 additions & 8 deletions core/src/test/scala/configs/DeriveTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ import scalaprops.{Gen, Lazy, Properties, Scalaprops}
import scalaprops.ScalapropsScalaz._
import scalaz.{Apply, Equal}

// Note:
// the following case class needs to be declared outside and before DeriveTest object, because otherwise the generation
// of default value functions in scala 2.11 might occur after macro expansion
case class OptionDefault(opt: Option[Int] = Some(42))
object OptionDefault {
implicit lazy val equal: Equal[OptionDefault] =
Equal.equalA[OptionDefault]
}

object DeriveTest extends Scalaprops {

Expand Down Expand Up @@ -146,14 +154,6 @@ object DeriveTest extends Scalaprops {
p1 x p2
}


case class OptionDefault(opt: Option[Int] = Some(42))

object OptionDefault {
implicit lazy val equal: Equal[OptionDefault] =
Equal.equalA[OptionDefault]
}

val optionDefault = {
val p1 =
Properties.single("missing", forAll {
Expand Down
26 changes: 26 additions & 0 deletions core/src/test/scala/configs/instance/CaseClassTypesTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package configs.instance

import com.typesafe.config.ConfigFactory
import configs.{ConfigKeyNaming, ConfigReader}
import scalaprops.Property.forAll
import scalaprops.Scalaprops

object CaseClassTypesTest extends Scalaprops {

case class TestClass(myAttr1: String, myAttr2: String)

val caseClassMultiNaming = {
implicit val naming = ConfigKeyNaming.lowerCamelCase[TestClass].or(ConfigKeyNaming.hyphenSeparated[TestClass].apply)
forAll {
val reader = ConfigReader.derive[TestClass](naming)
val configStr = """
my-attr-1 = test
myAttr2 = test
"""
val config = ConfigFactory.parseString(configStr)
val d = reader.extract(config)
println(d)
d.isSuccess
}
}
}
1 change: 1 addition & 0 deletions project/Common.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ object Common extends AutoPlugin {
"-language:higherKinds",
"-language:implicitConversions",
"-language:experimental.macros"
//"-Ymacro-debug-lite"
),
scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.3.2
sbt.version=1.3.13