diff --git a/build.sbt b/build.sbt index 2130bf8..824f7f7 100644 --- a/build.sbt +++ b/build.sbt @@ -55,11 +55,12 @@ javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") /* dependencies */ libraryDependencies <++= scalaVersion { sv => Seq( - "org.specs2" %% "specs2" % "2.3.11" % "test", - "org.scalacheck" %% "scalacheck" % "1.11.3" % "test", - "com.chuusai" %% "shapeless" % "2.0.0" % "test", - "com.typesafe" % "config" % "1.2.1", - "org.scala-lang" % "scala-reflect" % sv % "provided") + "org.specs2" %% "specs2" % "2.3.11" % "test", + "org.scalacheck" %% "scalacheck" % "1.11.3" % "test", + "com.chuusai" %% "shapeless" % "2.0.0" % "test", + "com.typesafe" % "config" % "1.2.1", + "com.google.guava" % "guava" % "18.0", + "org.scala-lang" % "scala-reflect" % sv % "provided") } /* you may need these repos */ diff --git a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala index 18f3599..b0d9281 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala @@ -4,6 +4,7 @@ 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] @@ -11,22 +12,48 @@ trait ArbitraryTypeReader { 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] @@ -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 @@ -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._ @@ -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]) diff --git a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala index 01793f0..a8b857c 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala @@ -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 @@ -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._ @@ -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) }