aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/core/rest/Swagger.scala
blob: b598b33e38d9c0a87b7f46f7ff5877df2eb1de4b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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.util.control.NonFatal

class Swagger(
    override val host: String,
    accessSchemes: List[String],
    version: String,
    override val apiClasses: Set[Class[_]],
    val config: Config,
    val logger: Logger)
    extends SwaggerHttpService {

  override val schemes = accessSchemes.map { s =>
    Scheme.forValue(s)
  }

  // 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)

      val paths = if (swagger.getPaths == null) {
        Map.empty
      } else {
        swagger.getPaths.asScala
      }

      // Removing trailing spaces
      val fixedPaths = paths.map {
        case (key, path) =>
          key.trim -> path
      }

      swagger.setPaths(fixedPaths.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")

}