为 Swagger 实现 Enumeratum 支持

Implementing Enumeratum support for Swagger

我正在使用 Swagger 来注释我的 API,而在我们的 API 中,我们非常依赖 enumeratum。如果我什么都不做,swagger 不会识别它,只会调用它 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 对象。

我必须采取什么解决方案来避免重复注释中的值?

这包括代码,因此太大而无法发表评论。

I tried, that wouldn't work because the @ApiModel annotation wants a String constant as a value (and not a reference to a constant)

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

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. 可用范围内的两个属性