Play framework and reading a scala annotation

298 Views Asked by At

I am struggling to get a value from a scala annotation in a Play controller method.

I defined a class for the annotation:

case class Auth(perm: String) extends scala.annotation.StaticAnnotation

Then I am reading it in one of the Play's filters:

import scala.reflect.runtime.{universe => u}

val res = u.runtimeMirror(handlerDef.classLoader)
  .classSymbol(Class.forName(handlerDef.controller))
  .info
  .decls
  .find(_.name.toString == handlerDef.method)
  .flatMap(_.asMethod.annotations.find(_.tree.tpe =:= u.typeOf[Auth]))

Now, I am getting Option[Annotation] and when I println it, it's: Some(Auth("test")) - the value that I put in the annotation, so it's all good.

I can't wrap my head around how to actually convert Annotation to my Auth.

Help is appreciated, thanks!

1

There are 1 best solutions below

0
On

Scaladoc of scala.reflect.api.Annotations says

Unlike Java reflection, Scala reflection does not support evaluation of constructor invocations stored in annotations into underlying objects. For instance it's impossible to go from @ann(1, 2) class C to ann(1, 2), so one has to analyze trees representing annotation arguments to manually extract corresponding values. Towards that end, arguments of an annotation can be obtained via annotation.tree.children.tail.

https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/api/Annotations.scala#L39-L42

You can use Toolbox to evaluate annotation tree

// libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value
import scala.tools.reflect.ToolBox

val rm = u.runtimeMirror(handlerDef.classLoader)

val tb = rm.mkToolBox() 

res.map(a => tb.eval(tb.untypecheck(a.tree)).asInstanceOf[Auth]) // Some(Auth(some permission))

https://docs.scala-lang.org/overviews/reflection/symbols-trees-types.html#tree-creation-via-parse-on-toolboxes

Calling a method from Annotation using reflection

Or (e.g. if you want to depend on scala-reflect only and not on scala-compiler) you can evaluate the tree manually:

res.map(a => {
  val arg = a.tree.children.tail match { case List(q"${s: String}") => s }
  Auth(arg)
}) // Some(Auth(some permission))

or

res.map(_.tree match {
  case q"new ${t@TypeTree()}(${s: String})" /*if t.tpe == typeOf[Auth]*/ => Auth(s)
}) // Some(Auth(some permission))

Notice that the tree will not match pattern q"new com.example.Auth(${s: String})" because annotation trees have different shape.

By the way, with rm.staticClass(...) instead of rm.classSymbol(Class.forName(...)) you can use Scala class name (e.g. org.example.App.MyClass) instead of Java class name (e.g. org.example.App$MyClass). Also you can try scala.reflect.runtime.currentMirror instead of u.runtimeMirror(classLoader). Also .decls.find(_.name.toString == methodName) can be replaced with .decl(u.TermName(methodName)).

Just in case, if you know types of class and annotation and method name at compile time then you can do the same using compile-time reflection

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

def getMethodAnnotation[Cls, Ann](methodName: String): Ann = macro getMethodAnnotationImpl[Cls, Ann]

def getMethodAnnotationImpl[Cls: c.WeakTypeTag, Ann: c.WeakTypeTag](c: blackbox.Context)(methodName: c.Tree): c.Tree = {
  import c.universe._

  val q"${methodNameStr: String}" = methodName

  weakTypeOf[Cls]
    .decl(TermName(methodNameStr))
    .annotations.find(_.tree.tpe =:= weakTypeOf[Ann]).get.tree match {
    case q"new ${t: TypeTree}[..$targs](...$argss)" => q"new ${t.tpe}[..$targs](...$argss)"
  }
}

class MyClass {
  @Auth("some permission")
  def myMethod(): Unit = ()
}

getMethodAnnotation[MyClass, Auth]("myMethod") //scalac: new Auth("some permission")