Scalameta:确定特定的注释
问题描述:
我想使用scalameta注释宏在Scala中自动生成REST API模型。具体地,给出:Scalameta:确定特定的注释
@Resource case class User(
@get id : Int,
@get @post @patch name : String,
@get @post email : String,
registeredOn : Long
)
我要生成:
object User {
case class Get(id: Int, name: String, email: String)
case class Post(name: String, email: String)
case class Patch(name: Option[String])
}
trait UserRepo {
def getAll: Seq[User.Get]
def get(id: Int): User.Get
def create(request: User.Post): User.Get
def replace(id: Int, request: User.Put): User.Get
def update(id: Int, request: User.Patch): User.Get
def delete(id: Int): User.Get
}
我有东西在这里工作:https://github.com/pathikrit/metarest
具体我这样做:
import scala.collection.immutable.Seq
import scala.collection.mutable
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.meta._
class get extends StaticAnnotation
class put extends StaticAnnotation
class post extends StaticAnnotation
class patch extends StaticAnnotation
@compileTimeOnly("@metarest.Resource not expanded")
class Resource extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
val (cls: Defn.Class, companion: Defn.Object) = defn match {
case Term.Block(Seq(cls: Defn.Class, companion: Defn.Object)) => (cls, companion)
case cls: Defn.Class => (cls, q"object ${Term.Name(cls.name.value)} {}")
case _ => abort("@metarest.Resource must annotate a class")
}
val paramsWithAnnotation = for {
Term.Param(mods, name, decltype, default) <- cls.ctor.paramss.flatten
seenMods = mutable.Set.empty[String]
modifier <- mods if seenMods.add(modifier.toString)
(tpe, defArg) <- modifier match {
case mod"@get" | mod"@put" | mod"@post" => Some(decltype -> default)
case mod"@patch" =>
val optDeclType = decltype.collect({case tpe: Type => targ"Option[$tpe]"})
val defaultArg = default match {
case Some(term) => q"Some($term)"
case None => q"None"
}
Some(optDeclType -> Some(defaultArg))
case _ => None
}
} yield modifier -> Term.Param(Nil, name, tpe, defArg)
val models = paramsWithAnnotation
.groupBy(_._1.toString)
.map({case (verb, pairs) =>
val className = Type.Name(verb.stripPrefix("@").capitalize)
val classParams = pairs.map(_._2)
q"case class $className[..${cls.tparams}] (..$classParams)"
})
val newCompanion = companion.copy(
templ = companion.templ.copy(stats = Some(
companion.templ.stats.getOrElse(Nil) ++ models
))
)
Term.Block(Seq(cls, newCompanion))
}
}
我不爽用下面的代码片段:
modifier match {
case mod"@get" | mod"@put" | mod"@post" => ...
case mod"@patch" => ...
case _ => None
}
上面的代码对我的注释进行“串行”模式匹配。反正是有重新使用模式匹配的精确注释我有这些:
class get extends StaticAnnotation
class put extends StaticAnnotation
class post extends StaticAnnotation
class patch extends StaticAnnotation
答
这是可能的替换使用位运行时反射的get()
提取的[email protected]
stringly类型的注释(在编译时)。 此外,假设我们还希望允许用户与@metarest.get
或@_root_.metarest.get
完全限定注释所有下面的代码示例假设import scala.meta._
。的@get
,@metarest.get
和@_root_.metarest.get
树结构是
@ mod"@get".structure
res4: String = """ Mod.Annot(Ctor.Ref.Name("get"))
"""
@ mod"@metarest.get".structure
res5: String = """
Mod.Annot(Ctor.Ref.Select(Term.Name("metarest"), Ctor.Ref.Name("get")))
"""
@ mod"@_root_.metarest.get".structure
res6: String = """
Mod.Annot(Ctor.Ref.Select(Term.Select(Term.Name("_root_"), Term.Name("metarest")), Ctor.Ref.Name("get")))
"""
的选择要么是Ctor.Ref.Select
或Term.Select
和名称是Term.Name
或Ctor.Ref.Name
。
让我们首先创建一个自定义选择提取
object Select {
def unapply(tree: Tree): Option[(Term, Name)] = tree match {
case Term.Select(a, b) => Some(a -> b)
case Ctor.Ref.Select(a, b) => Some(a -> b)
case _ => None
}
}
然后创建一些帮助工具,
object ParamAnnotation {
/* isSuffix(c, a.b.c) // true
* isSuffix(b.c, a.b.c) // true
* isSuffix(a.b.c, a.b.c) // true
* isSuffix(_root_.a.b.c, a.b.c) // true
* isSuffix(d.c, a.b.c) // false
*/
def isSuffix(maybeSuffix: Term, fullName: Term): Boolean =
(maybeSuffix, fullName) match {
case (a: Name, b: Name) => a.value == b.value
case (Select(q"_root_", a), b: Name) => a.value == b.value
case (a: Name, Select(_, b)) => a.value == b.value
case (Select(aRest, a), Select(bRest, b)) =>
a.value == b.value && isSuffix(aRest, bRest)
case _ => false
}
// Returns true if `mod` matches the tree structure of `@T`
def modMatchesType[T: ClassTag](mod: Mod): Boolean = mod match {
case Mod.Annot(term: Term.Ref) =>
isSuffix(term, termRefForType[T])
case _ => false
}
// Parses `T.getClass.getName` into a Term.Ref
// Uses runtime reflection, but this happens only at compile time.
def termRefForType[T](implicit ev: ClassTag[T]): Term.Ref =
ev.runtimeClass.getName.parse[Term].get.asInstanceOf[Term.Ref]
}
有了这个设置,我们可以将同伴对象添加到get
定义与 unapply
布尔提取器
class get extends StaticAnnotation
object get {
def unapply(mod: Mod): Boolean = ParamAnnotation.modMatchesType[get](mod)
}
做同样的post
和put
,我们现在可以编写
// before
case mod"@get" | mod"@put" | mod"@post" => Some(decltype -> default)
// after
case get() | put() | post() => Some(decltype -> default)
注意的是,如果用户重命名,例如get
进口
import metarest.{get => GET}
我会建议中止如果这种方法仍然无法正常工作注释与预期不符
// before
case _ => None
// after
case unexpected => abort("Unexpected modifier $unexpected. Expected one of: put, get post")
PS。object get { def unapply(mod: Mod): Boolean = ... }
部分是可以由一些@ParamAnnotation
宏注释生成的样板文件,例如@ParamAnnotion class get extends StaticAnnotation
我在scalameta/scalameta中打开了一个PR来将“选择”提取器添加到contrib模块。 https://github.com/scalameta/scalameta/pull/800 –
我还在scalameta/paradise中打开了一张票,默认提供'@ParamAnnotation' https://github.com/scalameta/paradise/issues/193 –