Skip to content

Commit

Permalink
Add support for classes to @safeConfig.
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisNeveu committed Jan 21, 2016
1 parent 3920b6c commit e414d8b
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 144 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,38 @@ override def onStart(app: Application): Unit = {
}
```

## How to use
## How To Use
The `safeConfig` annotation marks a configuration object. Within the configuration object all errors are handled automatically and accessors are created exposing the pure values. Additionally, configuration objects expose the [`ConfigApi`](http://gawkermedia.github.io/safe-config/doc/#com.kinja.config.ConfigApi) interface (select "Visibility: All").

All config values in the configuration object are eagerly evaluated and if any are the wrong type or missing, an exception is thrown indicating the problems with reading the config file.
![](http://gawkermedia.github.io/safe-config/img/BootupErrorsException.png)

In order to catch these errors as soon as possible, you should reference your config objects during your application's startup.

## Use With Classes
As of version 1.1.0, Safe Config can be used to annotate a class as well. This works well with Play 2.4's dependency injection. Instead of passing the underlying play config to the macro directly, pass the name of the identifier it is available at within the class object.

```scala
import com.kinja.config.safeConfig

import play.api._, ApplicationLoader.Context

@safeConfig("rawConfig")
class Bootstrap(context: Context) extends BuiltInComponentsFromContext(context) {
private val rawConfig = configuration.underlying

val dbConfig = for {
conf dbConfig
read conf.getString("read")
write conf.getString("write")
} yield DbConfig(read, write)

val languages = getStringList("application.languages")

val secret = getString("application.secret")
}


## API Documentation

The full API documentation is available [here](http://gawkermedia.github.io/safe-config/doc/#package).
Expand Down Expand Up @@ -90,3 +114,9 @@ object Config extends com.kinja.config.ConfigApi {

final case class DbConfig(readConnection : String, writeConnection : String)
```

## Limitations

Due to the way type-checking occurs within the macro, forward references are not allowed within the annotated object.

Because of a [bug](https://github.com/scalamacros/paradise/issues/49) in Macro Paradise, annotation of objects nested within a class does not work.
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ object Build extends Build {
file("."),
settings = Defaults.defaultSettings ++ scalariformSettings ++ wartremoverSettings ++ Seq(
organization := "com.kinja",
version := "1.0.1-SNAPSHOT",
version := "1.1.0",
scalaVersion := "2.11.6",
pomExtra := pomStuff,
scalacOptions ++= Seq(
Expand Down
293 changes: 151 additions & 142 deletions src/main/scala/com/kinja/config/safeConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import scala.reflect.macros.{ blackbox, whitebox, TypecheckException }

import com.typesafe.config.{ Config TypesafeConfig }

/**
* Adding this annotation to a class or object enables the configuration DSL within.
*
* @param underlying Either the underlying Config object or a string indicating the identifier
* that the underlying Config object can be referenced by.
*/
@compileTimeOnly("Macro paradise must be enabled to expand macro annotations.")
class safeConfig(underlying : TypesafeConfig) extends StaticAnnotation {
class safeConfig(underlying : Any) extends StaticAnnotation {
def macroTransform(annottees : Any*) : Any = macro safeConfig.impl
}

Expand All @@ -20,169 +26,172 @@ object safeConfig {
def freshType(): TypeName = TypeName(c.freshName("safe_config"))

// Get the argument passed to safeConfig.
@SuppressWarnings(Array("org.brianmckenna.wartremover.warts.IsInstanceOf"))
val underlying : Tree = c.prefix.tree match {
case q"new $_(..$params)" params match {
case Literal(Constant(i: String)) :: Nil Ident(TermName(i))
case head :: Nil head
case _ c.abort(c.enclosingPosition, "No underlying template given.")
case _ c.abort(c.enclosingPosition, "No underlying configuration given.")
}
case _ c.abort(c.enclosingPosition, "Encountered unexpected tree.")
}

val bootupErrors = tq"com.kinja.config.BootupErrors"
val liftedTypesafeConfig = tq"com.kinja.config.LiftedTypesafeConfig"

// Create a stub for a function in the ConfigApi interface.
def apiStub(name : String, typ : Tree) =
q"""def ${TermName(name)}(name : String) : $bootupErrors[$typ] = (throw new Exception("")) : $bootupErrors[$typ]"""

// The interface of ConfigApi for type-checking.
val configApiStubs = List(
q"""val root : $bootupErrors[$liftedTypesafeConfig] = (throw new Exception("")) : $bootupErrors[$liftedTypesafeConfig]""",
// format: OFF
apiStub("getBoolean", tq"Boolean"),
apiStub("getBooleanList", tq"List[Boolean]"),
apiStub("getConfig", tq"$liftedTypesafeConfig"),
apiStub("getDouble", tq"Double"),
apiStub("getDoubleList", tq"List[Double]"),
apiStub("getDuration", tq"scala.concurrent.duration.Duration"),
apiStub("getDurationList", tq"List[scala.concurrent.duration.Duration]"),
apiStub("getInt", tq"Int"),
apiStub("getIntList", tq"List[Int]"),
apiStub("getLong", tq"Long"),
apiStub("getLongList", tq"List[Long]"),
apiStub("getObject", tq"com.typesafe.config.ConfigObject"),
apiStub("getObjectList", tq"List[com.typesafe.config.ConfigObject]"),
apiStub("getString", tq"String"),
apiStub("getStringList", tq"List[String]"),
apiStub("getRawConfig", tq"com.typesafe.config.Config"))
// format: ON

// The root element.
val root = q"""val root = com.kinja.config.BootupErrors(com.kinja.config.LiftedTypesafeConfig($underlying, "root"))"""
val bootupErrors = tq"com.kinja.config.BootupErrors"
val liftedTypesafeConfig = tq"com.kinja.config.LiftedTypesafeConfig"

// The root element.
val root = q"""lazy val root = com.kinja.config.BootupErrors(com.kinja.config.LiftedTypesafeConfig($underlying, "root"))"""

def modBody(impl : Template): Template = {
val configApi = tq"com.kinja.config.ConfigApi"
val newParents = impl.parents.filter {
case Select(Ident(TermName("scala")), TypeName("AnyRef")) false
case _ true
} :+ configApi

// Put members of mixins like ConfigApi into scope.
val context = c.typecheck(q"""(throw new java.lang.Exception("")) : Object with ..$newParents""")
val dummyMembers = context.tpe.members.filter(member =>
member.name.decodedName.toString.trim != "wait"
).map {
case m: MethodSymbol =>
val params = m.paramLists.headOption.toList.flatMap(_.map(p => q"""val ${p.name.toTermName} : ${p.typeSignature}"""))
q"""def ${m.name.toTermName}(..$params) : ${m.returnType} = (throw new java.lang.Exception("")) : ${m.returnType}"""
case t: TermSymbol => q"""var ${t.name.toTermName} : ${t.typeSignature} = (throw new java.lang.Exception("")) : ${t.typeSignature}"""
}

val output = annottees.head.tree match {
case ModuleDef(mods, name, impl)
val configApi = tq"com.kinja.config.ConfigApi"
val newParents = configApi :: (impl.parents.filter(_ == tq"scala.AnyRef"))

// Previous definitions. Used for type checking.
// TODO: Typecheck the whole block at once to allow for forward references.
// This will require some serious reworking of the implementation.
var thusFar: List[Tree] = configApiStubs
val configValues = impl.body.flatMap {
case t @ ValDef(mods, name, tpt, rhs) if tpt.isEmpty && !mods.hasFlag(PRIVATE)
val typ = try {
c.typecheck(Block(thusFar, rhs)).tpe
} catch {
case e : TypecheckException c.abort(e.pos.asInstanceOf[c.Position], e.getMessage)
}
val Modifiers(flags, pw, ann) = mods
thusFar = thusFar :+ ValDef(mods, name, tq"$typ", rhs)

// Ignore pure values.
if (typ <:< typeOf[BootupErrors[_]])
List(name ValDef(Modifiers(PRIVATE | flags, pw, ann), freshTerm(), tq"$typ", rhs))
else
List.empty
case t @ ValDef(mods, name, tpt @ AppliedTypeTree(Ident(TypeName("BootupErrors")), args), rhs)
if !mods.hasFlag(PRIVATE)

val Modifiers(flags, pw, ann) = mods
thusFar = thusFar :+ t
List(name ValDef(Modifiers(PRIVATE | flags, pw, ann), freshTerm(), tpt, rhs))
case DefDef(_, name, _, _, _, _) if name.decodedName.toString == "<init>" List.empty
case t
thusFar = thusFar :+ t
// Previous definitions. Used for type checking.
// TODO: Typecheck the whole block at once to allow for forward references.
// This will require some serious reworking of the implementation.
var thusFar: List[Tree] = dummyMembers.toList
val configValues = impl.body.flatMap {
case t @ ValDef(mods, name, tpt, rhs) if tpt.isEmpty && !mods.hasFlag(PRIVATE)
val typ = try {
c.typecheck(Block(thusFar, rhs)).tpe
} catch {
case e : TypecheckException c.abort(e.pos.asInstanceOf[c.Position], e.getMessage)
}
val Modifiers(flags, pw, ann) = mods
thusFar = thusFar :+ ValDef(mods, name, tq"$typ", rhs)

// Ignore pure values.
if (typ <:< typeOf[BootupErrors[_]])
List(name ValDef(Modifiers(PRIVATE | flags, pw, ann), freshTerm(), tq"$typ", rhs))
else
List.empty
}
case t @ ValDef(mods, name, tpt @ AppliedTypeTree(Ident(TypeName("BootupErrors")), args), rhs)
if !mods.hasFlag(PRIVATE)

val Modifiers(flags, pw, ann) = mods
thusFar = thusFar :+ t
List(name ValDef(Modifiers(PRIVATE | flags, pw, ann), freshTerm(), tpt, rhs))
case DefDef(_, name, _, _, _, _) if name.decodedName.toString == "<init>" List.empty
case t @ ValDef(mods, name, tpt, rhs) if mods.hasFlag(Flag.PARAMACCESSOR)

val Modifiers(_, pw, ann) = mods
val flags = if (mods.hasFlag(Flag.IMPLICIT)) Flag.IMPLICIT else NoFlags

thusFar = thusFar :+ ValDef(Modifiers(flags, pw, ann), name, tpt, rhs)
List.empty
case t
thusFar = thusFar :+ t
List.empty
}

// Replaces references between config values with the private name.
val transformer = new Transformer {
import collection.mutable.Stack
val stack : Stack[Map[TermName, TermName]] = Stack(configValues.map {
case (key, tree) key tree.name
} toMap)

override def transform(tree : Tree) : Tree = tree match {
case Block(stmnts, last)
val (args, trees) = stmnts.foldLeft(stack.head -> List.empty[Tree]) {
case ((idents, block), t @ ValDef(_, name, _, _))
val args = idents - name
stack.push(args)
try (
args (super.transform(t) :: block)
) finally { stack.pop(); () }
case ((idents, block), t)
idents (super.transform(t) :: block)
}
stack.push(args)
try super.transform(tree) finally { stack.pop(); () }
case Function(valDefs, _)
val args = stack.head -- valDefs.map(_.name)
stack.push(args)
try super.transform(tree) finally { stack.pop(); () }
case Ident(TermName(name)) if stack.head.contains(TermName(name))
val anonName = stack.head.get(TermName(name)).get
Ident(anonName)
case _ super.transform(tree)
}
// Replaces references between config values with the private name.
val transformer = new Transformer {
import collection.mutable.Stack
val stack : Stack[Map[TermName, TermName]] = Stack(configValues.map {
case (key, tree) key tree.name
} toMap)

override def transform(tree : Tree) : Tree = tree match {
case Block(stmnts, last)
val (args, trees) = stmnts.foldLeft(stack.head -> List.empty[Tree]) {
case ((idents, block), t @ ValDef(_, name, _, _))
val args = idents - name
stack.push(args)
try (
args (super.transform(t) :: block)
) finally { stack.pop(); () }
case ((idents, block), t)
idents (super.transform(t) :: block)
}
stack.push(args)
try super.transform(tree) finally { stack.pop(); () }
case Function(valDefs, _)
val args = stack.head -- valDefs.map(_.name)
stack.push(args)
try super.transform(tree) finally { stack.pop(); () }
case Ident(TermName(name)) if stack.head.contains(TermName(name))
val anonName = stack.head.get(TermName(name)).get
Ident(anonName)
case _ super.transform(tree)
}
}

// The maximum number of arguments to a class in Java is 255.
val extractors = configValues.grouped(255).flatMap { configValues
val extractorName = freshType()
val extractorClass = {
val classMembers = configValues.map(_._2).flatMap {
case ValDef(Modifiers(flags, pw, ann), name, AppliedTypeTree(tpt, args), rhs)
List(ValDef(Modifiers(NoFlags, pw, ann), name, args.head, EmptyTree))
case ValDef(Modifiers(flags, pw, ann), name, tpt, rhs) tpt.tpe match {
case TypeRef(_, _, typ :: Nil)
List(ValDef(Modifiers(NoFlags, pw, ann), name, tq"$typ", EmptyTree))
case _ List.empty
}
}
val construct = classMembers.foldRight(q"new $extractorName(..${classMembers.map(_.name)})") {
case (valDef, acc) q"($valDef$acc)"
// The maximum number of arguments to a class in Java is 255.
val extractors = configValues.grouped(255).flatMap { configValues
val extractorName = freshType()
val extractorClass = {
val classMembers = configValues.map(_._2).flatMap {
case ValDef(Modifiers(flags, pw, ann), name, AppliedTypeTree(tpt, args), rhs)
List(ValDef(Modifiers(NoFlags, pw, ann), name, args.head, EmptyTree))
case ValDef(Modifiers(flags, pw, ann), name, tpt, rhs) tpt.tpe match {
case TypeRef(_, _, typ :: Nil)
List(ValDef(Modifiers(NoFlags, pw, ann), name, tq"$typ", EmptyTree))
case _ List.empty
}
q"""private final class $extractorName(..$classMembers)
private object ${extractorName.toTermName} {
def construct = $construct
}""".children
}
val extractor = {
val constructor =
if (configValues.length > 1)
q"${extractorName.toTermName}.construct"
else
q"${extractorName.toTermName}.construct"

val applied = configValues.foldLeft(q"com.kinja.config.BootupErrors($constructor)") {
case (acc, (_, valDef)) q"$acc <*> ${valDef.name}"
}
val construct = classMembers.foldRight(q"new $extractorName(..${classMembers.map(_.name)})") {
case (valDef, acc) q"($valDef$acc)"
}
q"""private final class $extractorName(..$classMembers)
private object ${extractorName.toTermName} {
def construct = $construct
}""".children
}
val extractor = {
val constructor =
if (configValues.length > 1)
q"${extractorName.toTermName}.construct"
else
q"${extractorName.toTermName}.construct"

val applied = configValues.foldLeft(q"com.kinja.config.BootupErrors($constructor)") {
case (acc, (_, valDef)) q"$acc <*> ${valDef.name}"
}

val extractorInstance = freshTerm()

val bundled = q"""private val $extractorInstance = ($applied)
.fold(errs => throw new com.kinja.config.BootupConfigurationException(errs), a => a)"""
val extractorInstance = freshTerm()

val accessors = configValues.map {
case (name, valDef) q"val $name = $extractorInstance.${valDef.name}"
}
val bundled = q"""private val $extractorInstance = ($applied)
.fold(errs => throw new com.kinja.config.BootupConfigurationException(errs), a => a)"""

(extractorClass :+ bundled) ++ accessors
val accessors = configValues.map {
case (name, valDef) q"val $name = $extractorInstance.${valDef.name}"
}
extractor
}

val otherMembers = impl.body.filter {
case ValDef(_, name, _, _) if configValues.exists(_._1 == name) false
case _ true
(extractorClass :+ bundled) ++ accessors
}
val finalConfigValues = transformer.transformTrees(configValues.map(_._2))
extractor
}

val newBody = root :: (otherMembers ++ finalConfigValues ++ extractors)
val otherMembers = impl.body.filter {
case ValDef(_, name, _, _) if configValues.exists(_._1 == name) false
case _ true
}
val finalConfigValues = transformer.transformTrees(configValues.map(_._2))

val newBody = root :: (otherMembers ++ finalConfigValues ++ extractors)

Template(newParents, impl.self, newBody)
}

ModuleDef(mods, name, Template(newParents, impl.self, newBody))
val output = annottees.head.tree match {
case ModuleDef(mods, name, impl)
ModuleDef(mods, name, modBody(impl))
case ClassDef(mods, name, tparams, impl)
ClassDef(mods, name, tparams, modBody(impl))
case _ c.abort(c.enclosingPosition, "Config must be an object.")
}
// println(output)
Expand Down
7 changes: 7 additions & 0 deletions src/test/scala/com/kinja/config/DependencyInjection.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kinja.config

import com.typesafe.config._

abstract class DependencyInjection {
lazy val injectedConfig = ConfigFactory.load()
}
Loading

0 comments on commit e414d8b

Please sign in to comment.