【问题标题】:Scalameta: Identify particular annotationsScalameta:识别特定的注释
【发布时间】:2017-09-09 16:47:09
【问题描述】:

我想使用 Scalameta 注释宏在 Scala 中自动生成 REST API 模型。具体来说,给定:

@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

【问题讨论】:

    标签: scala scala-macros scala-reflect scala-macro-paradise scalameta


    【解决方案1】:

    可以使用一些运行时反射(在编译时)将mod@get 字符串类型注释替换为get() 提取器。 此外,假设我们还希望允许用户使用@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")
    

    附言。 object get { def unapply(mod: Mod): Boolean = ... } 部分是样板文件,可以由一些 @ParamAnnotation 宏注释生成,例如 @ParamAnnotion class get extends StaticAnnotation

    【讨论】:

    猜你喜欢
    • 2021-06-22
    • 2014-05-24
    • 1970-01-01
    • 2018-05-17
    • 2014-07-09
    • 2018-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多