Skip to content

Instantly share code, notes, and snippets.

@ccw
Last active January 7, 2017 04:18
Show Gist options
  • Save ccw/8d7c16ca90519f4492a25c299f5d3c25 to your computer and use it in GitHub Desktop.
Save ccw/8d7c16ca90519f4492a25c299f5d3c25 to your computer and use it in GitHub Desktop.
FinatraSwagger parameter resolution example
package com.github.xiaodongw.swagger.finatra
import com.google.inject.Guice
import com.twitter.finatra.json.Parameterizable
import com.twitter.finagle.http.MediaType
import com.twitter.finatra.json.ParameterizableResolver._
import com.twitter.inject.Injector
import io.swagger.models._
import io.swagger.models.parameters._
import io.swagger.models.properties.{ArrayProperty, Property, RefProperty}
import io.swagger.util.Json
import scala.collection.JavaConversions._
import scala.language.implicitConversions
import scala.reflect.runtime.currentMirror
import scala.reflect.runtime.universe._
object FinatraOperation {
implicit def convert(operation: Operation)
(implicit swagger: Swagger): FinatraOperation =
new FinatraOperation(operation)
}
class FinatraOperation(op: Operation)(implicit swagger: Swagger) {
import FinatraSwagger._
private implicit val resolver = Some(resolveAndRegister(_))
/**
* Registers route, query and body parameters based on the given case class definitions
*/
def request[T <: Parameterizable : TypeTag]: Operation = {
implicit val resolver = Some(resolveAndRegister(_))
val (_, props) = resolveTypeProperties[T].filter(_.property.isDefined).foldLeft((op, Seq[Property]())) { (o, p) =>
val pTypes = p.annotations.groupBy { ann =>
ann.annotationType.getSimpleName match {
case "RouteParam" =>
"route"
case "QueryParam" =>
"query"
case _ =>
"body"
}
}
val prop = p.property.get
if (pTypes.contains("route")) {
o._1.parameter(new PathParameter().name(prop.getName).property(prop))
o
} else if (pTypes.contains("query")) {
o._1.parameter(new QueryParameter().name(prop.getName).property(prop))
o
} else {
(o._1, o._2 :+ prop)
}
}
if (props.nonEmpty) {
val param = new BodyParameter().name("body").schema(
new RefModel(registerModule(currentMirror.runtimeClass(typeOf[T]).getSimpleName, props))
)
param.setRequired(true)
op.parameter(param)
}
op
}
protected def registerModule(name: String, props: Seq[Property]): String = {
swagger.addDefinition(name, props.foldLeft(new ModelImpl().`type`("object")) { (m, p) =>
m.property(p.getName, p)
})
name
}
protected def resolveAndRegister(clazz: Class[_]): Property = {
if (classOf[Parameterizable].isAssignableFrom(clazz)) {
new RefProperty(registerModule(clazz.getSimpleName,
resolveClassProperties(clazz.asInstanceOf[Class[Parameterizable]])))
} else {
swagger.registerModel(clazz)
}
}
protected implicit def op2p(props: Seq[ParameterizableProperty]): Seq[Property] = props.filter(_.property.isDefined).map(_.property.get)
}
package com.github.xiaodongw.swagger.finatra
import io.swagger.converter.ModelConverters
import io.swagger.models.properties.Property
import io.swagger.models.{Operation, Path, Swagger}
import scala.collection.JavaConverters._
import scala.reflect.runtime._
import scala.reflect.runtime.universe._
import scala.language.implicitConversions
object FinatraSwagger {
private val finatraRouteParamter = ":(\\w+)".r
implicit def convertToFinatraSwagger(swagger: Swagger): FinatraSwagger = new FinatraSwagger(swagger)
}
class FinatraSwagger(swagger: Swagger) {
def registerModel[T: TypeTag]: Property = {
val paramType: Type = typeOf[T]
if(paramType =:= TypeTag.Nothing.tpe) {
null
} else {
registerModel(currentMirror.runtimeClass(paramType))
}
}
def registerModel(typeClass: java.lang.reflect.Type): Property = {
val modelConverters = ModelConverters.getInstance()
val models = modelConverters.readAll(typeClass)
for(entry <- models.entrySet().asScala) {
swagger.addDefinition(entry.getKey, entry.getValue)
}
modelConverters.readAsProperty(typeClass)
}
def convertPath(path: String): String = {
FinatraSwagger.finatraRouteParamter.replaceAllIn(path, "{$1}")
}
def registerOperation(path: String, method: String, operation: Operation): Swagger = {
val swaggerPath = convertPath(path)
swagger.path(swaggerPath,
Option(swagger.getPath(swaggerPath)).getOrElse(new Path()).set(method, operation))
}
}
package com.twitter.finatra.json
import java.lang.annotation.Annotation
import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy
import com.twitter.finatra.json.internal.caseclass.reflection.{CaseClassSigParser, ScalaType}
import com.twitter.finatra.request.{QueryParam, RouteParam}
import io.swagger.models.properties.{ArrayProperty, MapProperty, ObjectProperty, Property}
import scala.collection.JavaConversions._
import scala.language.{existentials, implicitConversions}
import scala.reflect.runtime._
import scala.reflect.runtime.universe._
trait Parameterizable {}
object ParameterizableResolver {
private type Resolver = Class[_] => Property
private val translator = new SnakeCaseStrategy()
case class ParameterizableProperty(
name: String,
property: Option[Property],
annotations: Array[Annotation]
)
/**
* Resolves the given parameterizable type and returns all its property definitions
*
* @param resolver the resolver to convert class definition into swagger property
* @tparam T sub type of [[Parameterizable]]
*/
def resolveTypeProperties[T <: Parameterizable : TypeTag](implicit resolver: Option[Resolver] = None): Seq[ParameterizableProperty] = {
resolveClassProperties(currentMirror.runtimeClass(typeOf[T]).asInstanceOf[Class[T]])
}
/**
* Resolves the given parameterizable class and returns all its property definitions
*
* @param clazz subclass of [[Parameterizable]]
* @param resolver the resolver to convert class definition into swagger property
* @return
*/
def resolveClassProperties(clazz: Class[_ <: Parameterizable])
(implicit resolver: Option[Resolver] = None): Seq[ParameterizableProperty] = {
CaseClassSigParser.parseConstructorParams(clazz).map { p =>
val prop = resolver.map { implicit r =>
val dps = resolveScalaType(p.scalaType)
dps.setName(snakify(p.name))
dps
}
(p.name, prop)
}.zip(clazz.getConstructors.head.getParameterAnnotations).map { p =>
ParameterizableProperty(p._1._1, p._1._2, p._2)
}
}
def resolveURLParameters[Q <: Parameterizable : TypeTag](q: Q) : Map[String, _] = {
implicit val resolver: Option[Resolver] = None
val clazz = currentMirror.runtimeClass(typeOf[Q])
resolveTypeProperties[Q].foldLeft(Map[String, Any]()) { (s, e) =>
if (e.annotations.exists(a => a.isInstanceOf[RouteParam] || a.isInstanceOf[QueryParam])) {
clazz.getMethod(e.name).invoke(q) match {
case o: Option[_] =>
if (o.isDefined) {
s + (snakify(e.name) -> o.get)
} else s
case v =>
s + (snakify(e.name) -> v)
}
} else {
s
}
}
}
protected def snakify(name : String): String = translator.translate(name)
protected def resolveScalaType(t: ScalaType)(implicit resolver: Resolver): Property = {
if (t.isMap) {
new MapProperty().additionalProperties(resolveScalaType(t.typeArguments.last))
} else if (t.isCollection) {
new ArrayProperty(resolveScalaType(t.typeArguments.head))
} else {
t.typeRefType.symbol.toString match {
case "scala.Option" =>
val p = resolveScalaType(t.typeArguments.head)
p.setRequired(false)
p
case _ =>
resolver(t.runtimeClass)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment