Skip to content

Commit

Permalink
working annotation based settings
Browse files Browse the repository at this point in the history
  • Loading branch information
frohoff committed May 13, 2015
1 parent 8f60a99 commit 54cb7d1
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 33 deletions.
81 changes: 50 additions & 31 deletions src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,65 @@ import com.typesafe.config.Config
import scala.language.experimental.macros
import scala.reflect.internal.{StdNames, SymbolTable, Definitions}
import com.google.common.base.CaseFormat
import scala.annotation.StaticAnnotation
import scala.collection.JavaConversions._

trait ArbitraryTypeReader {
@ArbitraryTypeReader.settings(identity, false)
implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T]
}

object ArbitraryTypeReader extends ArbitraryTypeReader
object ArbitraryTypeReader extends ArbitraryTypeReader {
class settings(paramKeyMapper: String => String = identity[String], exhaustiveMapping: Boolean = false) extends StaticAnnotation
}

trait HyphenCaseArbitraryTypeReader {
implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReaderWithHyphenCase[T]
trait OtherArbitraryTypeReader {
@ArbitraryTypeReader.settings(_.toLowerCase, true)
implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T]
}

object HyphenCaseArbitraryTypeReader extends HyphenCaseArbitraryTypeReader
object OtherArbitraryTypeReader extends OtherArbitraryTypeReader

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

trait NameMapper {
def map(name: String): String
def assertExhaustive(cfg: Config, path: String, mappedPaths: Seq[String]): Unit = {
val relevantPaths = cfg.getObject(path).keySet.map(k => s"$path.$k")
val unmappedPaths = relevantPaths -- mappedPaths
if (! unmappedPaths.isEmpty) throw new IllegalArgumentException(s"config paths not exhaustively mapped: $unmappedPaths")
}

object defaultMapper extends NameMapper {
def map(name: String) = name
}
def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
import c.universe._

object hyphenCaseMapper extends NameMapper {
def map(name: String) = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, name)
}
try {
val annotationParamExpr
= c.macroApplication.symbol.annotations
.filter(_.tree.tpe <:< typeOf[ArbitraryTypeReader.settings]).head.tree.children.tail

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

//println(paramMapperExpr)

reify {
val readerExpr = reify {
new ValueReader[T] {
def read(config: Config, path: String): T = instantiateFromConfig[T](c, paramMapper)(
val paramMapper: String => String = c.Expr[String => String](c.resetLocalAttrs(paramMapperExpr)).splice
val exhaustiveMapping: Boolean = c.Expr[Boolean](c.resetLocalAttrs(exhaustiveMappingExpr)).splice
def read(config: Config, path: String): T = instantiateFromConfig[T](c)(
config = c.Expr[Config](Ident(TermName("config"))),
path = c.Expr[String](Ident(TermName("path")))).splice
}
}
}

def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
arbitraryTypeValueReaderWithParamMapper(c, defaultMapper)
}
//println(show(readerExpr))

def arbitraryTypeValueReaderWithHyphenCase[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
arbitraryTypeValueReaderWithParamMapper(c, hyphenCaseMapper)
readerExpr
} catch { // FIXME: remove
case e => e.printStackTrace(); throw e
}
}

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

val returnType = c.weakTypeOf[T]
Expand All @@ -67,29 +77,38 @@ object ArbitraryTypeReaderMacros {

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

val instantiationArgs = extractMethodArgsFromConfig[T](c, paramMapper)(method = instantiationMethod,
val instantiationArgsAndKeys = extractMethodArgsFromConfig[T](c)(method = instantiationMethod,
companionObjectMaybe = companionSymbol, config = config, path = path, fail = fail)
val (instantiationArgs, instantiationKeys) = instantiationArgsAndKeys.unzip
val instantiationObject = companionSymbol.filterNot(_ =>
instantiationMethod.isConstructor
).map(Ident(_)).getOrElse(New(Ident(returnType.typeSymbol)))
val instantiationCall = Select(instantiationObject, instantiationMethod.name)
c.Expr[T](Apply(instantiationCall, instantiationArgs))

val assertion = reify {
assertExhaustive(config.splice, path.splice, c.Expr[List[String]](q"List(..$instantiationKeys)").splice)
}

val conditionalAssertion = If(Ident(TermName("exhaustiveMapping")), assertion.tree, Literal(Constant(())))
val instantiation = Apply(instantiationCall, instantiationArgs)
c.Expr[T](Block(List(conditionalAssertion), instantiation))
}

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] = {
def extractMethodArgsFromConfig[T : c.WeakTypeTag](c: Context)(method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol],
config: c.Expr[Config], path: c.Expr[String], fail: String => Nothing): List[(c.Tree, c.Tree)] = {
import c.universe._

val decodedMethodName = method.name.decodedName.toString

if (!method.isPublic) fail(s"'$decodedMethodName' method is not public")

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

companionObjectMaybe.filter(_ => param.asTerm.isParamWithDefault) map { companionObject =>
(companionObjectMaybe.filter(_ => param.asTerm.isParamWithDefault) map { companionObject =>
// class has companion object and param has default
val optionType = appliedType(weakTypeOf[Option[_]].typeConstructor, List(returnType))
val optionReaderType = appliedType(weakTypeOf[ValueReader[_]].typeConstructor, List(optionType))
val optionReader = c.inferImplicitValue(optionReaderType, silent = true) match {
Expand All @@ -110,7 +129,7 @@ object ArbitraryTypeReaderMacros {
case x => x
}
q"$reader.read($config, $key)"
}
}, key)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2"""
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
throw an exception with non-exhuastive key mapping $withExhaustivityCheck
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 @@ -88,12 +89,23 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2"""

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

def withExhaustivityCheck = {
import Ficus.{stringValueReader}
import OtherArbitraryTypeReader._
val cfg = ConfigFactory.parseString(s"withCamelCaseFields { acamelcasefield: foo, anothercamelcasefield: bar, anextrafield: error }")
val reader = arbitraryTypeValueReader[ClassWithCamelCaseFields]
def doRead: Unit = reader.read(cfg, "withCamelCaseFields")
doRead must throwA[IllegalArgumentException].like {
case e:IllegalArgumentException => e.getMessage should contain("withCamelCaseFields.anextrafield")
}
}

def fallBackToApplyMethodDefaultValue = {
import Ficus.{optionValueReader, stringValueReader}
import ArbitraryTypeReader._
Expand Down

0 comments on commit 54cb7d1

Please sign in to comment.