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

hyphen-case config key support #13

Closed
wants to merge 1 commit into from
Closed
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
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ description := "A Scala-friendly wrapper companion for Typesafe config"

startYear := Some(2013)

licenses := Seq(
"MIT License" -> url("http://www.opensource.org/licenses/mit-license.html")
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed here, license is moved to project/Publish.scala


/* scala versions and options */
scalaVersion := "2.11.7"
Expand Down Expand Up @@ -32,7 +35,7 @@ scalacOptions <++= scalaVersion map { sv =>
"-language:implicitConversions",
"-language:higherKinds"
)
else
else
List("-target:jvm-1.8")
}

Expand All @@ -45,6 +48,7 @@ libraryDependencies <++= scalaVersion { sv =>
"org.scalacheck" %% "scalacheck" % "1.11.3" % "test",
"com.chuusai" %% "shapeless" % "2.0.0" % "test",
"com.typesafe" % "config" % "1.3.0",
"com.google.guava" % "guava" % "18.0",
"org.scala-lang" % "scala-reflect" % sv % "provided")
}

Expand All @@ -71,4 +75,3 @@ mappings in (Compile, packageBin) ~= { (ms: Seq[(File, String)]) =>

Publish.settings


39 changes: 33 additions & 6 deletions src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,56 @@ import net.ceedubs.ficus.util.ReflectionUtils
import com.typesafe.config.Config
import scala.language.experimental.macros
import scala.reflect.internal.{StdNames, SymbolTable, Definitions}
import com.google.common.base.CaseFormat

trait ArbitraryTypeReader {
implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T]
}

object ArbitraryTypeReader extends ArbitraryTypeReader

trait HyphenCaseArbitraryTypeReader {
implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReaderWithHyphenCase[T]
}

object HyphenCaseArbitraryTypeReader extends HyphenCaseArbitraryTypeReader

object ArbitraryTypeReaderMacros {
import scala.reflect.macros.blackbox.Context

def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
trait NameMapper {
def map(name: String): String
}

object defaultMapper extends NameMapper {
def map(name: String) = name
}

object hyphenCaseMapper extends NameMapper {
def map(name: String) = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, name)
}

def arbitraryTypeValueReaderWithParamMapper[T : c.WeakTypeTag](c: Context, paramMapper: NameMapper): c.Expr[ValueReader[T]] = {
import c.universe._

reify {
new ValueReader[T] {
def read(config: Config, path: String): T = instantiateFromConfig[T](c)(
def read(config: Config, path: String): T = instantiateFromConfig[T](c, paramMapper)(
config = c.Expr[Config](Ident(TermName("config"))),
path = c.Expr[String](Ident(TermName("path")))).splice
}
}
}

def instantiateFromConfig[T : c.WeakTypeTag](c: Context)(config: c.Expr[Config], path: c.Expr[String]): c.Expr[T] = {
def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
arbitraryTypeValueReaderWithParamMapper(c, defaultMapper)
}

def arbitraryTypeValueReaderWithHyphenCase[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
arbitraryTypeValueReaderWithParamMapper(c, hyphenCaseMapper)
}

def instantiateFromConfig[T : c.WeakTypeTag](c: Context, paramMapper: NameMapper)(config: c.Expr[Config], path: c.Expr[String]): c.Expr[T] = {
import c.universe._

val returnType = c.weakTypeOf[T]
Expand All @@ -40,7 +67,7 @@ object ArbitraryTypeReaderMacros {

val instantiationMethod = ReflectionUtils.instantiationMethod[T](c, fail)

val instantiationArgs = extractMethodArgsFromConfig[T](c)(method = instantiationMethod,
val instantiationArgs = extractMethodArgsFromConfig[T](c, paramMapper)(method = instantiationMethod,
companionObjectMaybe = companionSymbol, config = config, path = path, fail = fail)
val instantiationObject = companionSymbol.filterNot(_ =>
instantiationMethod.isConstructor
Expand All @@ -49,7 +76,7 @@ object ArbitraryTypeReaderMacros {
c.Expr[T](Apply(instantiationCall, instantiationArgs))
}

def extractMethodArgsFromConfig[T : c.WeakTypeTag](c: Context)(method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol],
def extractMethodArgsFromConfig[T : c.WeakTypeTag](c: Context, paramMapper: NameMapper)(method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol],
config: c.Expr[Config], path: c.Expr[String], fail: String => Nothing): List[c.Tree] = {
import c.universe._

Expand All @@ -58,7 +85,7 @@ object ArbitraryTypeReaderMacros {
if (!method.isPublic) fail(s"'$decodedMethodName' method is not public")

method.paramLists.head.zipWithIndex map { case (param, index) =>
val name = param.name.decodedName.toString
val name = paramMapper.map(param.name.decodedName.toString)
val key = q"""$path + "." + $name"""
val returnType: Type = param.typeSignatureIn(c.weakTypeOf[T])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2"""
instantiate with no apply method but a single constructor with multiple params $instantiateMultiParamConstructor
instantiate with multiple apply methods if only one returns the correct type $multipleApply
instantiate with primary constructor when no apply methods and multiple constructors $multipleConstructors
instantiate with camel case fields and hyphen case config keys $withCamelCaseFields
use another implicit value reader for a field $withOptionField
fall back to a default value on an apply method $fallBackToApplyMethodDefaultValue
fall back to default values on an apply method if base key isn't in config $fallBackToApplyMethodDefaultValueNoKey
Expand Down Expand Up @@ -85,6 +86,14 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2"""
instance.foo must_== foo
}

def withCamelCaseFields = {
import Ficus.{stringValueReader}
import HyphenCaseArbitraryTypeReader._
val cfg = ConfigFactory.parseString(s"withCamelCaseFields { a-camel-case-field: foo, another-camel-case-field: bar }")
val instance = arbitraryTypeValueReader[ClassWithCamelCaseFields].read(cfg, "withCamelCaseFields")
(instance.aCamelCaseField must_== "foo") and (instance.anotherCamelCaseField must_== "bar")
}

def fallBackToApplyMethodDefaultValue = {
import Ficus.{optionValueReader, stringValueReader}
import ArbitraryTypeReader._
Expand Down Expand Up @@ -247,8 +256,9 @@ object ArbitraryTypeReaderSpec {
case class WithReaderInCompanion(foo: String)

object WithReaderInCompanion {
implicit val reader: ValueReader[WithReaderInCompanion] =
implicit val reader: ValueReader[WithReaderInCompanion] =
ValueReader.relative(_ => WithReaderInCompanion("from-companion"))
}

class ClassWithCamelCaseFields(val aCamelCaseField: String, val anotherCamelCaseField: String)
}