为Swagger实施Enumeratum支持

时间:2017-12-18 13:16:55

标签: scala macros swagger scala-macros

我使用Swagger来注释我的API,在我们的API中,我们非常依赖enumeratum。如果我什么都不做,那么昂首阔步就不会认出它,只需称它为object

例如,我有这个代码可以工作:

sealed trait Mode extends EnumEntry

object Mode extends Enum[Mode] {
  override def values = findValues

  case object Initial extends Mode
  case object Delta extends Mode
}

@ApiModel
case class Foobar(
  @ApiModelProperty(dataType = "string", allowedValues = "Initial,Delta")
  mode: Mode
)

但是,我想避免重复这些值,因为我的某些类型比这个例子有更多;我不想手动保持同步。

问题是@ApiModel想要一个常量引用,所以我不能做reference = Mode.values.mkString(",")之类的事情。

我确实尝试了一个带有宏天堂的宏,通常所以我可以写:

@EnumeratumApiModel(Mode)
sealed trait Mode extends EnumEntry

object Mode extends Enum[Mode] {
  override def values = findValues

  case object Initial extends Mode
  case object Delta extends Mode
}

...但它不起作用,因为宏传递无法访问Mode对象。

我有什么解决方法可以避免重复注释中的值?

1 个答案:

答案 0 :(得分:1)

这包括代码,因此对于评论来说太大了。

  

我试过,这是行不通的,因为@ApiModel注释需要一个String常量作为值(而不是对常量的引用)

这段代码编译对我来说很合适(注意你应该避免明确指定类型):

import io.swagger.annotations._
import enumeratum._

@ApiModel(reference = Mode.reference)
sealed trait Mode extends EnumEntry

object Mode extends Enum[Mode] {
  final val reference = "enum(Initial,Delta)"           // this works!
  //final val reference: String = "enum(Initial,Delta)" // surprisingly this doesn't!

  override def values = findValues

  case object Initial extends Mode
  case object Delta extends Mode
}

因此,使用另一个生成此类reference字符串的宏似乎已足够了,我假设您已经有一个(或者您可以根据EnumMacros.findValuesImpl的代码创建一个)。

<强>更新

以下是POC的一些代码,它实际上可以正常工作。首先,您要开始关注macro annotation

import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.reflect.macros.whitebox.Context
import scala.collection.immutable._


@compileTimeOnly("enable macro to expand macro annotations")
class SwaggerEnumContainer extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro SwaggerEnumMacros.genListString
}

@compileTimeOnly("enable macro to expand macro annotations")
class SwaggerEnumValue(val readOnly: Boolean = false, val required: Boolean = false) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro SwaggerEnumMacros.genParamAnnotation

}


class SwaggerEnumMacros(val c: Context) {

  import c.universe._

  def genListString(annottees: c.Expr[Any]*): c.Expr[Any] = {

    val result = annottees.map(_.tree).toList match {
      case (xxx@q"object $name extends ..$parents { ..$body }") :: Nil =>
        val enclosingObject = xxx.asInstanceOf[ModuleDef]
        val q"${tq"$pname[..$ptargs]"}(...$pargss)" = parents.head
        val enumTraitIdent = ptargs.head.asInstanceOf[Ident]
        val subclassSymbols: List[TermName] = enclosingObject.impl.body.foldLeft(List.empty[TermName])((list, innerTree) => {
          innerTree match {
            case innerObj: ModuleDefApi =>
              val innerParentIdent = innerObj.impl.parents.head.asInstanceOf[Ident]
              if (enumTraitIdent.name.equals(innerParentIdent.name))
                innerObj.name :: list
              else
                list

            case _ => list
          }
        })

        val reference = subclassSymbols.map(n => n.encodedName.toString).mkString(",")
        q"""
                object $name extends ..$parents {
                  final val allowableValues = $reference
                  ..$body
                }
              """

    }
    c.Expr[Any](result)
  }

  def genParamAnnotation(annottees: c.Expr[Any]*): c.Expr[Any] = {
    val annotationParams: AnnotationParams = extractAnnotationParameters(c.prefix.tree)
    val baseSwaggerAnnot =
      q""" new ApiModelProperty(
                   dataType = "string",
                   allowableValues = Mode.allowableValues
                   ) """.asInstanceOf[Apply] // why I have to force cast?

    val swaggerAnnot: c.universe.Apply = annotationParams.addArgsTo(baseSwaggerAnnot)

    annottees.map(_.tree).toList match {
      // field definition
      case List(param: ValDef) => c.Expr[Any](decorateValDef(param, swaggerAnnot))
      // field in a case class = constructor param
      case (param: ValDef) :: (rest@(_ :: _)) => decorateConstructorVal(param, rest, swaggerAnnot)
      case _ => c.abort(c.enclosingPosition, "SwaggerEnumValue is expected to be used for value definitions")
    }
  }

  def decorateValDef(valDef: ValDef, swaggerAnnot: Apply): ValDef = {
    val q"$mods val $name: $tpt = $rhs" = valDef
    val newMods: Modifiers = mods.mapAnnotations(al => swaggerAnnot :: al)
    q"$newMods val $name: $tpt = $rhs"
  }


  def decorateConstructorVal(annottee: c.universe.ValDef, expandees: List[Tree], swaggerAnnot: Apply): c.Expr[Any] = {
    val q"$_ val $tgtName: $_ = $_" = annottee
    val outputs = expandees.map {
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => {
        // paramss is a 2d array so map inside map
        val newParams: List[List[ValDef]] = paramss.map(_.map({
          case valDef: ValDef if valDef.name == tgtName => decorateValDef(valDef, swaggerAnnot)
          case otherParam => otherParam
        }))

        q"$mods class $tpname[..$tparams] $ctorMods(...$newParams) extends { ..$earlydefns } with ..$parents { $self => ..$stats }"
      }

      case otherTree => otherTree
    }
    c.Expr[Any](Block(outputs, Literal(Constant(()))))
  }


  case class AnnotationParams(readOnly: Boolean, required: Boolean) {
    def customCopy(name: String, value: Any) = {
      name match {
        case "readOnly" => copy(readOnly = value.asInstanceOf[Boolean])
        case "required" => copy(required = value.asInstanceOf[Boolean])
        case _ => c.abort(c.enclosingPosition, s"Unknown parameter '$name'")
      }
    }

    def addArgsTo(annot: Apply): Apply = {
      val additionalArgs: List[AssignOrNamedArg] = List(
        AssignOrNamedArg(q"readOnly", q"$readOnly"),
        AssignOrNamedArg(q"required", q"$required")
      )

      Apply(annot.fun, annot.args ++ additionalArgs)
    }
  }

  private def extractAnnotationParameters(tree: Tree): AnnotationParams = tree match {
    case ap: Apply =>
      val argNames = Array("readOnly", "required")
      val defaults = AnnotationParams(readOnly = false, required = false)

      ap.args.zipWithIndex.foldLeft(defaults)((acc, argAndIndex) => argAndIndex match {
        case (lit: Literal, index: Int) => acc.customCopy(argNames(index), c.eval(c.Expr[Any](lit)))

        case (namedArg: AssignOrNamedArg, _: Int) =>
          val q"$name = $lit" = namedArg
          acc.customCopy(name.asInstanceOf[Ident].name.toString, c.eval(c.Expr[Any](lit)))

        case _ => c.abort(c.enclosingPosition, "Failed to parse annotation params: " + argAndIndex)
      })
  }
}

然后你可以这样做:

sealed trait Mode extends EnumEntry

@SwaggerEnumContainer
object Mode extends Enum[Mode] {

  override def values = findValues

  case object Initial extends Mode
  case object Delta extends Mode
}


@ApiModel
case class Foobar(@ApiModelProperty(dataType = "string", allowableValues = Mode.allowableValues) mode: Mode)

或者你可以这样做我觉得有点干净

@ApiModel
case class Foobar2(
                    @SwaggerEnumValue mode: Mode,
                    @SwaggerEnumValue(true) mode2: Mode,
                    @SwaggerEnumValue(required = true) mode3: Mode,
                    i: Int, s: String = "abc") {
  @SwaggerEnumValue
  val modeField: Mode = Mode.Delta
}

请注意,这仍然只是一个POC。已知的缺陷包括:

  1. @SwaggerEnumContainer无法处理某些假allowableValues已经定义了一些假值(可能更适合IDE)
  2. @SwaggerEnumValue仅支持原始@ApiModelProperty
  3. 中可用范围内的两个属性