3

我想使用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 

回答

3

這是可能的替換使用位運行時反射的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.SelectTerm.Select和名稱是Term.NameCtor.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) 
} 

做同樣的postput,我們現在可以編寫

// 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

+0

我在scalameta/scalameta中打開了一個PR來將「選擇」提取器添加到contrib模塊。 https://github.com/scalameta/scalameta/pull/800 –

+1

我還在scalameta/paradise中打開了一張票,默認提供'@ParamAnnotation' https://github.com/scalameta/paradise/issues/193 –