Play framework and reading a scala annotation

282 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 answers

0
Dmytro Mitin 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")