Last active
January 7, 2017 04:18
-
-
Save ccw/8d7c16ca90519f4492a25c299f5d3c25 to your computer and use it in GitHub Desktop.
FinatraSwagger parameter resolution example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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