From 54cb7d12d09faabca05350cdfce23a9ff4253967 Mon Sep 17 00:00:00 2001 From: Chris Frohoff Date: Wed, 13 May 2015 09:16:32 -0700 Subject: [PATCH] working annotation based settings --- .../ficus/readers/ArbitraryTypeReader.scala | 81 ++++++++++++------- .../readers/ArbitraryTypeReaderSpec.scala | 16 +++- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala index b0d9281..7b291e7 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala @@ -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] @@ -67,17 +77,25 @@ 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 @@ -85,11 +103,12 @@ object ArbitraryTypeReaderMacros { 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 { @@ -110,7 +129,7 @@ object ArbitraryTypeReaderMacros { case x => x } q"$reader.read($config, $key)" - } + }, key) } } } diff --git a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala index a8b857c..2ce0696 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala @@ -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 @@ -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._