
com.github.xiaodongw.swagger.finatra.FinatraSwagger.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swagger-finatra_2.11 Show documentation
Show all versions of swagger-finatra_2.11 Show documentation
Add Swagger support for Finatra to generate REST API docuemnt
The newest version!
package com.github.xiaodongw.swagger.finatra
import com.fasterxml.jackson.databind.{JavaType, ObjectMapper}
import com.google.inject.{Inject => GInject}
import com.twitter.finagle.http.Request
import com.twitter.finatra.request.{FormParam, QueryParam, RouteParam, Header => HeaderParam}
import io.swagger.converter.{ModelConverter, ModelConverterContext, ModelConverters}
import io.swagger.jackson.ModelResolver
import io.swagger.models._
import io.swagger.models.parameters._
import io.swagger.models.properties.{Property, RefProperty}
import io.swagger.util.Json
import java.lang.annotation.Annotation
import java.lang.reflect.ParameterizedType
import java.util
import javax.inject.{Inject => JInject}
import net.bytebuddy.ByteBuddy
import net.bytebuddy.description.`type`.TypeDescription
import net.bytebuddy.description.modifier.Visibility
import scala.collection.JavaConverters._
import scala.collection.mutable
import scala.reflect.runtime._
import scala.reflect.runtime.universe._
object FinatraSwagger {
private val finatraRouteParamter = ":(\\w+)".r
/**
* Cache of dynamically generated class bodies keyed by qualified names
*/
private val dynamicClassBodies: mutable.HashMap[String, Class[_]] = new mutable.HashMap[String, Class[_]]()
implicit def convertToFinatraSwagger(swagger: Swagger): FinatraSwagger = new FinatraSwagger(swagger)
}
sealed trait ModelParam {
val name: String
val description: String
val required: Boolean
val typ: Class[_]
}
sealed trait FinatraRequestParam
case class RouteRequestParam(name: String, typ: Class[_], description: String = "", required: Boolean = true) extends FinatraRequestParam with ModelParam
case class QueryRequestParam(name: String, typ: Class[_], description: String = "", required: Boolean = true) extends FinatraRequestParam with ModelParam
case class BodyRequestParam(description: String = "", name: String, typ: Class[_], innerOptionType: Option[java.lang.reflect.Type] = None) extends FinatraRequestParam
case class RequestInjectRequestParam(name: String) extends FinatraRequestParam
case class HeaderRequestParam(name: String, required: Boolean = true, description: String = "", typ: Class[_]) extends FinatraRequestParam with ModelParam
case class FormRequestParam(name: String, description: String = "", required: Boolean = true, typ: Class[_]) extends FinatraRequestParam with ModelParam
object Resolvers {
class ScalaOptionResolver(objectMapper: ObjectMapper) extends ModelResolver(objectMapper) {
override def resolveProperty(
propType: JavaType,
context: ModelConverterContext,
annotations: Array[Annotation],
next: util.Iterator[ModelConverter]): Property = {
if (propType.getRawClass == classOf[Option[_]]) {
try {
return super.resolveProperty(propType.containedType(0), context, annotations, next)
} catch {
case _: Exception =>
}
}
super.resolveProperty(propType, context, annotations, next)
}
}
def register(objectMapper: ObjectMapper = Json.mapper): Unit = {
ModelConverters.getInstance().addConverter(new ScalaOptionResolver(objectMapper))
}
}
class FinatraSwagger(swagger: Swagger) {
import FinatraSwagger._
/**
* Register a request object that contains body information/route information/etc
*
* @tparam T
* @return
*/
def register[T: TypeTag]: List[Parameter] = {
val properties = getFinatraProps[T]
val className = currentMirror.runtimeClass(typeOf[T]).getName
val swaggerProps =
properties.collect {
case x: ModelParam => x
}.map {
case param @ (x: RouteRequestParam) =>
new PathParameter().
name(param.name).
description(param.description).
required(param.required).
property(registerModel(param.typ))
case param @ (x: QueryRequestParam) =>
new QueryParameter().
name(param.name).
description(param.description).
required(param.required).
property(registerModel(param.typ))
case param @ (x: HeaderRequestParam) =>
new HeaderParameter().
name(param.name).
description(param.description).
required(param.required).
property(registerModel(param.typ))
case param @ (x: FormRequestParam) =>
new FormParameter().
name(param.name).
description(param.description).
required(param.required).
property(registerModel(param.typ))
}
val bodyElements = properties.collect { case b: BodyRequestParam => b }
swaggerProps ++ List(registerDynamicBody(bodyElements, className)).flatten
}
/**
* Given the request object format its finatra parameters via reflection
*
* @tparam T
* @return
*/
private def getFinatraProps[T: TypeTag]: List[FinatraRequestParam] = {
val clazz = currentMirror.runtimeClass(typeOf[T])
val fields = clazz.getDeclaredFields
val constructorArgWithField =
clazz.
getConstructors.
head.getParameters.
map(m => (clazz: Class[_ <: Annotation]) => {
val annotation = m.getAnnotationsByType(clazz)
if (annotation.isEmpty) {
None
} else {
Some(annotation)
}
}).
zip(fields)
val ast: List[Option[FinatraRequestParam]] =
constructorArgWithField.map { case (annotationExtractor, field) =>
val routeParam = annotationExtractor(classOf[RouteParam])
val queryParam = annotationExtractor(classOf[QueryParam])
val injectJavax = annotationExtractor(classOf[JInject])
val injectGuice = annotationExtractor(classOf[GInject])
val header = annotationExtractor(classOf[HeaderParam])
val form = annotationExtractor(classOf[FormParam])
val (isRequired, innerOptionType) = field.getGenericType match {
case parameterizedType: ParameterizedType =>
val required = parameterizedType.getRawType.asInstanceOf[Class[_]] == classOf[Option[_]]
(required, Some(parameterizedType.getActualTypeArguments.apply(0)))
case _ =>
(true, None)
}
if (routeParam.isDefined) {
Some(RouteRequestParam(field.getName, typ = field.getType))
}
else if (queryParam.isDefined) {
Some(QueryRequestParam(field.getName, typ = field.getType, required = isRequired))
}
else if ((injectJavax.isDefined || injectGuice.isDefined) && field.getType.isAssignableFrom(classOf[Request])) {
Some(RequestInjectRequestParam(field.getName))
}
else if (header.isDefined) {
Some(HeaderRequestParam(field.getName, typ = field.getType, required = isRequired))
}
else if (form.isDefined) {
Some(FormRequestParam(field.getName, typ = field.getType, required = isRequired))
}
else {
Some(BodyRequestParam(name = field.getName, typ = field.getType, innerOptionType = innerOptionType))
}
}.toList
ast.flatten
}
private def emitBodyClassForElements(bodyElements: List[BodyRequestParam], className: String): Class[_] = {
val byteBuddy = new ByteBuddy()
// add "Body" to avoid name collisions
val bodyEmittedClass = byteBuddy.subclass(classOf[Object]).name(className)
val bodyFields = bodyElements.foldLeft(bodyEmittedClass) { (asm, body) =>
// if we have an inner option type, unwrap the option
// and pass it to the class builder so we can get proper
// definitions of the inner type in the swagger model
val bodyType = body.innerOptionType.getOrElse(body.typ).asInstanceOf[Class[_]]
asm.defineField(body.name, new TypeDescription.Generic.OfNonGenericType.ForLoadedType(bodyType), Visibility.PUBLIC)
}
bodyFields.make().load(getClass.getClassLoader).getLoaded
}
/**
* Creates a fake object for swagger to reflect upon
*
* @param bodyElements
* @param name
* @return
*/
private def registerDynamicBody(bodyElements: List[BodyRequestParam], name: String): Option[Parameter] = {
if (bodyElements.isEmpty) {
return None
}
val className = name + "Body"
val bodyClass = dynamicClassBodies.getOrElse(className, emitBodyClassForElements(bodyElements, className))
dynamicClassBodies.put(className, bodyClass)
val schema = registerModel(bodyClass, Some(name))
val model = schema match {
case null => null
case p: RefProperty => new RefModel(p.getSimpleRef)
case _ => null //todo map ArrayProperty to ArrayModel?
}
Some(
new BodyParameter().name("body").schema(model)
)
}
def registerModel[T: TypeTag]: Property = {
val paramType: Type = typeOf[T]
if (paramType =:= TypeTag.Nothing.tpe) {
null
} else {
val typeClass = currentMirror.runtimeClass(paramType)
registerModel(typeClass)
}
}
private def registerModel(typeClass: Class[_], name: Option[String] = None) = {
val modelConverters = ModelConverters.getInstance()
val models = modelConverters.readAll(typeClass)
for (entry <- models.entrySet().asScala) {
swagger.addDefinition(entry.getKey, entry.getValue)
}
val schema = modelConverters.readAsProperty(typeClass)
schema
}
def convertPath(path: String): String = {
FinatraSwagger.finatraRouteParamter.replaceAllIn(path, "{$1}")
}
def registerOperation(path: String, method: String, operation: Operation): Swagger = {
val swaggerPath = convertPath(path)
var spath = swagger.getPath(swaggerPath)
if (spath == null) {
spath = new Path()
swagger.path(swaggerPath, spath)
}
spath.set(method, operation)
swagger
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy