Skip to content

Commit

Permalink
Merge pull request #45 from smart-data-lake/feature/multiple-naming-s…
Browse files Browse the repository at this point in the history
…trategies

Feature/multiple naming strategies
  • Loading branch information
kxbmap authored Nov 11, 2020
2 parents 22ad412 + a13bb56 commit bda2bd6
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 90 deletions.
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

0 comments on commit bda2bd6

Please sign in to comment.