Skip to content
This repository has been archived by the owner on Jan 25, 2018. It is now read-only.

Commit

Permalink
Merge pull request #4 from broadinstitute/ks_api_routing
Browse files Browse the repository at this point in the history
Added a `WrappedRoute` for ultimately protecting /api.
  • Loading branch information
kshakir committed Oct 21, 2015
2 parents b2eeb79 + f30f001 commit 6479841
Show file tree
Hide file tree
Showing 6 changed files with 565 additions and 55 deletions.
172 changes: 141 additions & 31 deletions src/main/scala/lenthall/spray/SwaggerUiHttpService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,158 @@ import com.typesafe.config.Config
import spray.http.StatusCodes
import spray.routing.HttpService

/**
* Serves up the swagger UI from org.webjars/swagger-ui.
*/
trait SwaggerUiHttpService extends HttpService {
/**
* @return The version of the org.webjars/swagger-ui artifact. For example "2.1.1".
*/
def swaggerUiVersion: String

val swaggerUiInfo: SwaggerUiInfo

final def swaggerUiRoutes = {
swaggerUiInfo match {
case SwaggerUiInfo(swaggerUiVersion, baseUrl, docsPath, uiPath) =>
get {
pathPrefix(uiPath.split("/").map(segmentStringToPathMatcher).reduceLeft(_ / _)) {
// when the user hits the doc url, redirect to the index.html with api docs specified on the url
pathEndOrSingleSlash { context =>
context.redirect(s"$baseUrl/$uiPath/index.html?url=$baseUrl/$docsPath", StatusCodes.TemporaryRedirect)
} ~ getFromResourceDirectory(s"META-INF/resources/webjars/swagger-ui/$swaggerUiVersion")
}
}
/**
* Informs the swagger UI of the base of the application url, as hosted on the server.
* If your entire app is served under "http://myserver/myapp", then the base URL is "/myapp".
* If the app is served at the root of the application, leave this value as the empty string.
*
* @return The base URL used by the application, or the empty string if there is no base URL. For example "/myapp".
*/
def swaggerUiBaseUrl = ""

/**
* @return The path to the swagger UI html documents. For example "swagger"
*/
def swaggerUiPath = "swagger"

/**
* The path to the actual swagger documentation in either yaml or json, to be rendered by the swagger UI html.
*
* @return The path to the api documentation to render in the swagger UI.
* For example "api-docs" or "swagger/lenthall.yaml".
*/
def swaggerUiDocsPath = "api-docs"

/**
* @return When true, if someone requests / (or /baseUrl if setup), redirect to the swagger UI.
*/
def swaggerUiFromRoot = true

private def routeFromRoot = pathEndOrSingleSlash {
// Redirect / to the swagger UI
redirect(s"$swaggerUiBaseUrl/$swaggerUiPath", StatusCodes.TemporaryRedirect)
}

/**
* Serves up the swagger UI only. Redirects requests to the root of the UI path to the index.html.
*
* @return Route serving the swagger UI.
*/
final def swaggerUiRoute = {
val route = get {
pathPrefix(separateOnSlashes(swaggerUiPath)) {
// when the user hits the doc url, redirect to the index.html with api docs specified on the url
pathEndOrSingleSlash {
redirect(
s"$swaggerUiBaseUrl/$swaggerUiPath/index.html?url=$swaggerUiBaseUrl/$swaggerUiDocsPath",
StatusCodes.TemporaryRedirect)
} ~ getFromResourceDirectory(s"META-INF/resources/webjars/swagger-ui/$swaggerUiVersion")
}
}
if (swaggerUiFromRoot) route ~ routeFromRoot else route
}

}

/**
* Attributes to render an instance of Swagger UI.
*
* @param swaggerUiVersion Version of the swagger-ui bundle containing the resources, for example "2.1.1".
* @param baseUrl Base URL where the application is hosted.
* @param docsPath Endpoint for serving the api docs. Should NOT include the baseUrl.
* @param uiPath Path to serve the swagger-ui. Should NOT include the baseUrl.
* Extends the SwaggerUiHttpService to gets UI configuration values from a provided Typesafe Config.
*/
case class SwaggerUiInfo(swaggerUiVersion: String,
baseUrl: String = "",
docsPath: String = "api-docs",
uiPath: String = "swagger")

trait ConfigSwaggerUiHttpService extends SwaggerUiHttpService {
trait SwaggerUiConfigHttpService extends SwaggerUiHttpService {
/**
* @return The swagger UI config.
*/
def swaggerUiConfig: Config

import lenthall.config.ScalaConfig._

val swaggerUiInfo = {
var info = SwaggerUiInfo(swaggerUiConfig.getString("uiVersion"))
info = swaggerUiConfig.getStringOption("baseUrl").map(x => info.copy(baseUrl = x)) getOrElse info
info = swaggerUiConfig.getStringOption("docsPath").map(x => info.copy(docsPath = x)) getOrElse info
info = swaggerUiConfig.getStringOption("uiPath").map(x => info.copy(uiPath = x)) getOrElse info
info
override def swaggerUiVersion = swaggerUiConfig.getString("uiVersion")

abstract override def swaggerUiBaseUrl = swaggerUiConfig.getStringOr("baseUrl", super.swaggerUiBaseUrl)

abstract override def swaggerUiPath = swaggerUiConfig.getStringOr("uiPath", super.swaggerUiPath)

abstract override def swaggerUiDocsPath = swaggerUiConfig.getStringOr("docsPath", super.swaggerUiDocsPath)
}

/**
* An extension of HttpService to serve up a resource containing the swagger api as yaml or json. The resource
* directory and path on the classpath must match the path for route. The resource can be any file type supported by the
* swagger UI, but defaults to "yaml". This is an alternative to spray-swagger's SwaggerHttpService.
*/
trait SwaggerResourceHttpService extends HttpService {
/**
* @return The directory for the resource under the classpath, and in the url
*/
def swaggerDirectory = "swagger"

/**
* @return Name of the service, used to map the documentation resource at "/uiPath/serviceName.resourceType".
*/
def swaggerServiceName: String

/**
* @return The type of the resource, usually "yaml" or "json".
*/
def swaggerResourceType = "yaml"

/**
* Swagger needs HTTP OPTIONS requests to return 200 / OK. When true (the default), the swaggerResourceRoute will
* return 200 / OK for requests for OPTIONS on the swagger resource.
*
* See also:
* - https://github.com/swagger-api/swagger-ui/issues/1209
* - https://github.com/swagger-api/swagger-ui/issues/161
* - https://groups.google.com/forum/#!topic/swagger-swaggersocket/S6_I6FBjdZ8
*
* @return True if status code 200 should be returned for HTTP OPTIONS requests for the swagger resource.
*/
def swaggerResourceOptionsOk = true

/**
* @return The path to the swagger docs.
*/
protected def swaggerDocsPath = s"$swaggerDirectory/$swaggerServiceName.$swaggerResourceType"

/**
* @return A route that returns the swagger resource.
*/
final def swaggerResourceRoute = {
val swaggerDocsDirective = path(separateOnSlashes(swaggerDocsPath))
val route = get {
swaggerDocsDirective {
// Return /uiPath/serviceName.resourceType from the classpath resources.
getFromResource(swaggerDocsPath)
}
}

if (swaggerResourceOptionsOk) {
route ~ options {
swaggerDocsDirective {
// Also return 200 / OK for OPTIONS.
complete(StatusCodes.OK)
}
}
} else route
}
}

/**
* Extends the SwaggerUiHttpService and SwaggerResourceHttpService to serve up both.
*/
trait SwaggerUiResourceHttpService extends SwaggerUiHttpService with SwaggerResourceHttpService {
override def swaggerUiDocsPath = swaggerDocsPath

/**
* @return A route that redirects to the swagger UI and returns the swagger resource.
*/
final def swaggerUiResourceRoute = swaggerUiRoute ~ swaggerResourceRoute
}
31 changes: 31 additions & 0 deletions src/main/scala/lenthall/spray/WrappedRoute.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package lenthall.spray

import spray.routing._
import spray.routing.directives.PathDirectives

object WrappedRoute {

/**
* Wraps a route with a prefix.
* Can optionally serve the wrapped route, and the original unwrapped route, until clients are all switched over.
*
* @param unwrappedRoute The original unwrapped route.
*/
implicit class EnhancedWrappedRoute(val unwrappedRoute: Route) extends AnyVal {
/**
*
* @param wrappedPathPrefix The prefix to wrap unwrapped route. Ex: "api" (implicitly converted to a PathMatcher)
* @param routeUnwrapped For legacy reasons, should we also serve up the unwrapped route? Defaults to false.
* @return The wrappedRoute, followed optionally by the unwrappedRoute if routeUnwrapped is true.
*/
def wrapped(wrappedPathPrefix: PathMatcher0, routeUnwrapped: Boolean = false): Route = {
import PathDirectives._
import RouteConcatenation._
val route = pathPrefix(wrappedPathPrefix) {
unwrappedRoute
}
if (routeUnwrapped) route ~ unwrappedRoute else route
}
}

}
29 changes: 29 additions & 0 deletions src/test/resources/swagger/testservice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"swagger": "2.0",
"info": {
"title": "Test Service API",
"description": "Test Service API",
"version": "1.2.3"
},
"produces": [
"application/json"
],
"paths": {
"/hello": {
"get": {
"responses": {
"200": {
"description": "Says hello via get"
}
}
},
"post": {
"responses": {
"200": {
"description": "Says hello via post"
}
}
}
}
}
}
17 changes: 17 additions & 0 deletions src/test/resources/swagger/testservice.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
swagger: '2.0'
info:
title: Test Service API
description: Test Service API
version: 1.2.3
produces:
- application/json
paths:
/hello:
get:
responses:
'200':
description: Says hello via get
post:
responses:
'200':
description: Says hello via post
Loading

0 comments on commit 6479841

Please sign in to comment.