package xyz.driver.core.rest
import akka.http.scaladsl.model.{ContentType, ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.directives.FileAndResourceDirectives.ResourceFile
import akka.stream.ActorAttributes
import akka.stream.scaladsl.{Framing, StreamConverters}
import akka.util.ByteString
import com.github.swagger.akka.SwaggerHttpService
import com.github.swagger.akka.model._
import com.typesafe.config.Config
import com.typesafe.scalalogging.Logger
import io.swagger.models.Scheme
import io.swagger.util.Json
import scala.reflect.runtime.universe
import scala.reflect.runtime.universe.Type
import scala.util.control.NonFatal
class Swagger(
override val host: String,
override val schemes: List[Scheme],
version: String,
val apiTypes: Seq[Type],
val config: Config,
val logger: Logger)
extends SwaggerHttpService {
lazy val mirror = universe.runtimeMirror(getClass.getClassLoader)
override val apiClasses = apiTypes.map { tpe =>
mirror.runtimeClass(tpe.typeSymbol.asClass)
}.toSet
// Note that the reason for overriding this is a subtle chain of causality:
//
// 1. Some of our endpoints require a single trailing slash and will not
// function if it is omitted
// 2. Swagger omits trailing slashes in its generated api doc
// 3. To work around that, a space is added after the trailing slash in the
// swagger Path annotations
// 4. This space is removed manually in the code below
//
// TODO: Ideally we'd like to drop this custom override and fix the issue in
// 1, by dropping the slash requirement and accepting api endpoints with and
// without trailing slashes. This will require inspecting and potentially
// fixing all service endpoints.
override def generateSwaggerJson: String = {
import io.swagger.models.{Swagger => JSwagger}
import scala.collection.JavaConverters._
try {
val swagger: JSwagger = reader.read(apiClasses.asJava)
// Removing trailing spaces
swagger.setPaths(
swagger.getPaths.asScala
.map {
case (key, path) =>
key.trim -> path
}
.toMap
.asJava)
Json.pretty().writeValueAsString(swagger)
} catch {
case NonFatal(t) =>
logger.error("Issue with creating swagger.json", t)
throw t
}
}
override val basePath: String = config.getString("swagger.basePath")
override val apiDocsPath: String = config.getString("swagger.docsPath")
override val info = Info(
config.getString("swagger.apiInfo.description"),
version,
config.getString("swagger.apiInfo.title"),
config.getString("swagger.apiInfo.termsOfServiceUrl"),
contact = Some(
Contact(
config.getString("swagger.apiInfo.contact.name"),
config.getString("swagger.apiInfo.contact.url"),
config.getString("swagger.apiInfo.contact.email")
)),
license = Some(
License(
config.getString("swagger.apiInfo.license"),
config.getString("swagger.apiInfo.licenseUrl")
)),
vendorExtensions = Map.empty[String, AnyRef]
)
/** A very simple templating extractor. Gets a resource from the classpath and subsitutes any `{{key}}` with a value. */
private def getTemplatedResource(
resourceName: String,
contentType: ContentType,
substitution: (String, String)): Route = get {
Option(this.getClass.getClassLoader.getResource(resourceName)) flatMap ResourceFile.apply match {
case Some(ResourceFile(url, length @ _, _)) =>
extractSettings { settings =>
val stream = StreamConverters
.fromInputStream(() => url.openStream())
.withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))
.via(Framing.delimiter(ByteString("\n"), 4096, true).map(_.utf8String))
.map { line =>
line.replaceAll(s"\\{\\{${substitution._1}\\}\\}", substitution._2)
}
.map(line => ByteString(line + "\n"))
complete(
HttpEntity(contentType, stream)
)
}
case None => reject
}
}
def swaggerUI: Route =
pathEndOrSingleSlash {
getTemplatedResource(
"swagger-ui/index.html",
ContentTypes.`text/html(UTF-8)`,
"title" -> config.getString("swagger.apiInfo.title"))
} ~ getFromResourceDirectory("swagger-ui")
}