diff --git a/s2http/build.sbt b/s2http/build.sbt index ef020cd9..c094a6eb 100644 --- a/s2http/build.sbt +++ b/s2http/build.sbt @@ -18,6 +18,7 @@ */ lazy val akkaHttpVersion = "10.1.6" lazy val akkaVersion = "2.5.19" +lazy val swaggerUiWebjarsVersion = "3.20.9" name := "s2http" @@ -32,6 +33,8 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "org.webjars" % "swagger-ui" % swaggerUiWebjarsVersion, + "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test, "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test, diff --git a/s2http/src/main/resources/META-INF/resources/s2http/swagger/index.html b/s2http/src/main/resources/META-INF/resources/s2http/swagger/index.html new file mode 100644 index 00000000..68592576 --- /dev/null +++ b/s2http/src/main/resources/META-INF/resources/s2http/swagger/index.html @@ -0,0 +1,76 @@ + + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/s2http/src/main/resources/META-INF/resources/s2http/swagger/v0/swagger.json b/s2http/src/main/resources/META-INF/resources/s2http/swagger/v0/swagger.json new file mode 100644 index 00000000..a8d8bdaf --- /dev/null +++ b/s2http/src/main/resources/META-INF/resources/s2http/swagger/v0/swagger.json @@ -0,0 +1,1273 @@ +{ + "swagger":"2.0", + "info":{ + "description":"This is S2Graph HTTP Server.", + "version":"0.2", + "title":"S2Graph HTTP Server", + "contact":{ + "email":"dev@s2graph.incubator.apache.org" + }, + "license":{ + "name":"Apache 2.0", + "url":"http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "tags":[ + { + "name":"s2graph", + "description":"S2Graph HTTP Server", + "externalDocs":{ + "description":"Find out more about Apache S2Graph", + "url":"http://s2graph.incubator.apache.org/" + } + } + ], + "schemes":[ + "http", + "https" + ], + "paths":{ + "/admin/createService":{ + "post":{ + "tags":[ + "admin" + ], + "summary":"Create a service", + "description":"", + "operationId":"createService", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Service", + "required":true, + "schema":{ + "$ref":"#/definitions/Service" + } + } + ], + "responses":{ + "201":{ + "description":"Successfully created", + "schema":{ + "$ref":"#/definitions/ServiceResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/createLabel":{ + "post":{ + "tags":[ + "admin" + ], + "summary":"Create a label", + "description":"", + "operationId":"createLabel", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Service", + "required":true, + "schema":{ + "$ref":"#/definitions/Label" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/LabelResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/addIndex":{ + "post":{ + "tags":[ + "admin" + ], + "summary":"Add indices to a label", + "description":"", + "operationId":"addIndex", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Label indices to add", + "required":true, + "schema":{ + "$ref":"#/definitions/LabelIndices" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/LabelResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/getLabel/{name}":{ + "get":{ + "tags":[ + "admin" + ], + "summary":"Get general information on a label", + "description":"", + "operationId":"getLabelByName", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"name", + "description":"Label name", + "required":true, + "type":"string" + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/LabelResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/deleteLabelReally/{name}":{ + "put":{ + "tags":[ + "admin" + ], + "summary":"Delete a label", + "description":"", + "operationId":"deleteLabelByName", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"name", + "description":"Label name", + "required":true, + "type":"string" + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/Label" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/addProp/{name}":{ + "post":{ + "tags":[ + "admin" + ], + "summary":"Add a new property to the label", + "description":"", + "operationId":"addPropToLabel", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"name", + "description":"Label name", + "required":true, + "type":"string" + }, + { + "in":"body", + "name":"body", + "description":"Property definition", + "required":true, + "schema":{ + "$ref":"#/definitions/PropertyDefinition" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/LabelResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/admin/createServiceColumn":{ + "post":{ + "tags":[ + "admin" + ], + "summary":"Add a new property to the label", + "description":"", + "operationId":"createServiceColumn", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Service column definition", + "required":true, + "schema":{ + "$ref":"#/definitions/ServiceColumn" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/LabelResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/vertex/insert/{service}/{column}":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Create new vertices.", + "description":"", + "operationId":"insertVertices", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"service", + "description":"Service name.", + "required":true, + "type":"string" + }, + { + "in":"path", + "name":"column", + "description":"Column name.", + "required":true, + "type":"string" + }, + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/VerticesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/VerticesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/vertex/delete/{service}/{column}":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Delete vertices.", + "description":"", + "operationId":"deleteVertices", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"service", + "description":"Service name.", + "required":true, + "type":"string" + }, + { + "in":"path", + "name":"column", + "description":"Column name.", + "required":true, + "type":"string" + }, + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/VerticesRequest" + } + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/VerticesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/vertex/deleteAll/{service}/{column}":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Delete all vertices.", + "description":"", + "operationId":"deleteAllVertices", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"path", + "name":"service", + "description":"Service name.", + "required":true, + "type":"string" + }, + { + "in":"path", + "name":"column", + "description":"Column name.", + "required":true, + "type":"string" + }, + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/VerticesRequest" + } + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/VerticesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/edge/insert":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Create new edges.", + "description":"", + "operationId":"insertEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/edge/delete":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Delete edges.", + "description":"", + "operationId":"deleteEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/edge/update":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Update edges.", + "description":"", + "operationId":"updateEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/edge/increment":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Return edges with incremented values", + "description":"Works like update, other than it returns the incremented value and not the old value.", + "operationId":"incrementEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/mutate/edge/deleteAll":{ + "post":{ + "tags":[ + "mutate" + ], + "summary":"Delete all edges.", + "description":"", + "operationId":"deleteAllEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "201":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/graphs/getVertices":{ + "post":{ + "tags":[ + "query" + ], + "summary":"Select vertices.", + "description":"", + "operationId":"getVertices", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get vertices.", + "required":true, + "schema":{ + "$ref":"#/definitions/VerticesRequest" + } + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/VerticesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/graphs/checkEdges":{ + "post":{ + "tags":[ + "query" + ], + "summary":"Return edge for given vertex pair only if edge exist.", + "description":"This is more general way to check edge existence between any given vertex pairs comparing using _to on query parameter.", + "operationId":"checkEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + }, + "/graphs/getEdges":{ + "post":{ + "tags":[ + "query" + ], + "summary":"Select edges.", + "description":"", + "operationId":"getEdges", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Request to get edges.", + "required":true, + "schema":{ + "$ref":"#/definitions/EdgesRequest" + } + } + ], + "responses":{ + "200":{ + "description":"Successful operation", + "schema":{ + "$ref":"#/definitions/EdgesResponse" + } + }, + "400":{ + "description":"Bad request" + } + } + } + } + }, + "definitions":{ + "Service":{ + "type":"object", + "required":[ + "serviceName" + ], + "properties":{ + "serviceName":{ + "description":"User defined namespace", + "type":"string" + }, + "cluster":{ + "description":"Zookeeper quorum address", + "type":"string" + }, + "hTableName":{ + "description":"HBase table name", + "type":"string" + }, + "hTableTTL":{ + "description":"Time to live setting for data", + "type":"integer" + }, + "preSplitSize":{ + "description":"Factor for the table pre-split size", + "type":"integer" + } + } + }, + "Label":{ + "type":"object", + "required":[ + "label", + "srcServiceName", + "srcColumnName", + "srcColumnType", + "tgtColumnName", + "tgtColumnType" + ], + "properties":{ + "label":{ + "description":"Name of the relation", + "type":"string" + }, + "srcServiceName":{ + "description":"Source column’s service", + "type":"string" + }, + "srcColumnName":{ + "description":"Source column’s name", + "type":"string" + }, + "srcColumnType":{ + "description":"Source column’s data type", + "type":"string", + "enum":[ + "long", + "integer", + "string" + ] + }, + "tgtServiceName":{ + "description":"Target column’s service", + "type":"string" + }, + "tgtColumnName":{ + "description":"Target column’s name", + "type":"string" + }, + "tgtColumnType":{ + "description":"Target column’s data type", + "type":"string", + "enum":[ + "long", + "integer", + "string" + ] + }, + "indices":{ + "description":"Index definitions on the label", + "type":"array", + "items":{ + "$ref":"#/definitions/IndexDefinition" + } + }, + "props":{ + "description":"Property definitions on the label", + "type":"array", + "items":{ + "$ref":"#/definitions/PropertyDefinition" + } + }, + "isDirected":{ + "description":"Wether the label is directed or undirected", + "type":"integer" + }, + "serviceName":{ + "description":"Which service the label belongs to", + "type":"string" + }, + "hTableName":{ + "description":"A dedicated HBase table to your Label", + "type":"string" + }, + "hTableTTL":{ + "description":"Data time to live setting", + "type":"integer" + }, + "consistencyLevel":{ + "description":"If set to ‘strong’, only one edge is alowed between a pair of source/ target vertices. Set to ‘weak’, and multiple-edge is supported", + "type":"string", + "enum":[ + "weak", + "strong" + ] + } + } + }, + "LabelIndices":{ + "type":"object", + "required":[ + "label" + ], + "properties":{ + "label":{ + "description":"Name of the relation", + "type":"string" + }, + "indices":{ + "description":"Index definitions on the label", + "type":"array", + "items":{ + "$ref":"#/definitions/IndexDefinition" + } + } + } + }, + "IndexDefinition":{ + "type":"object", + "required":[ + "name", + "propNames" + ], + "properties":{ + "name":{ + "description":"Label index name", + "type":"string" + }, + "propNames":{ + "description":"Label property names used by the index", + "type":"array", + "items":{ + "type":"string" + } + }, + "dir":{ + "type":"string" + }, + "options":{ + "type":"string" + } + } + }, + "PropertyDefinition":{ + "type":"object", + "required":[ + "name", + "dataType" + ], + "properties":{ + "name":{ + "description":"Label property name", + "type":"string" + }, + "dataType":{ + "description":"Data type of the label property", + "type":"string" + }, + "defaultValue":{ + "description":"Default value of the label property", + "type":"string" + } + } + }, + "ServiceResponse":{ + "type":"object", + "required":[ + "status" + ], + "properties":{ + "status":{ + "description":"Status", + "type":"string" + }, + "messasge":{ + "$ref":"#/definitions/ServiceResponseMessage" + } + } + }, + "ServiceResponseMessage":{ + "type":"object", + "required":[ + "id", + "name" + ], + "properties":{ + "id":{ + "description":"Message ID", + "type":"string" + }, + "name":{ + "description":"User defined namespace", + "type":"string" + }, + "accessToken":{ + "description":"Access token", + "type":"string" + }, + "cluster":{ + "description":"Zookeeper quorum address", + "type":"string" + }, + "hTableName":{ + "description":"HBase table name", + "type":"string" + }, + "hTableTTL":{ + "description":"Time to live setting for data", + "type":"integer" + }, + "preSplitSize":{ + "description":"Factor for the table pre-split size", + "type":"integer" + } + } + }, + "LabelResponse":{ + "type":"object", + "required":[ + "status" + ], + "properties":{ + "status":{ + "description":"Status", + "type":"string" + }, + "messasge":{ + "$ref":"#/definitions/LabelResponseMessage" + } + } + }, + "LabelResponseMessage":{ + "type":"object", + "required":[ + "labelName" + ], + "properties":{ + "labelName":{ + "description":"The label name", + "type":"string" + }, + "from":{ + "$ref":"#/definitions/ServiceLabelColumn" + }, + "to":{ + "$ref":"#/definitions/ServiceLabelColumn" + }, + "isDirected":{ + "type":"boolean" + }, + "serviceName":{ + "description":"User defined namespace", + "type":"string" + }, + "consistencyLevel":{ + "description":"If set to ‘strong’, only one edge is alowed between a pair of source/ target vertices. Set to ‘weak’, and multiple-edge is supported", + "type":"string", + "enum":[ + "weak", + "strong" + ] + }, + "schemaVersion":{ + "type":"string" + }, + "isAsync":{ + "type":"boolean" + }, + "compressionAlgorithm":{ + "type":"string" + }, + "defaultIndex":{ + "$ref":"#/definitions/IndexDefinition" + }, + "extraIndex":{ + "type":"array", + "items":{ + "$ref":"#/definitions/IndexDefinition" + } + }, + "metaProps":{ + "type":"array", + "items":{ + "$ref":"#/definitions/PropertyDefinition" + } + }, + "options":{ + "type":"string" + } + } + }, + "ServiceColumn":{ + "type":"object", + "properties":{ + "serviceName":{ + "type":"string" + }, + "columnName":{ + "type":"string" + }, + "columnType":{ + "type":"string" + }, + "props":{ + "description":"Property definitions", + "type":"array", + "items":{ + "$ref":"#/definitions/PropertyDefinition" + } + } + } + }, + "ServiceLabelColumn":{ + "type":"object", + "properties":{ + "from":{ + "type":"string" + }, + "serviceName":{ + "type":"string" + }, + "columnName":{ + "type":"string" + }, + "columnType":{ + "type":"string" + } + } + }, + "VerticesRequest":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Vertex" + } + }, + "VerticesResponse":{ + "type":"object", + "properties":{ + "size":{ + "type":"integer" + }, + "degrees":{ + "type":"array", + "items":{ + } + }, + "results":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Vertex" + } + }, + "impressionId":{ + "type":"integer" + } + } + }, + "EdgesRequest":{ + "type":"object", + "properties":{ + "srcVertices":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Vertex" + } + }, + "steps":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Step" + } + } + } + }, + "EdgesResponse":{ + "type":"object", + "properties":{ + "size":{ + "type":"integer" + }, + "degrees":{ + "type":"array", + "items":{ + } + }, + "results":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Edge" + } + }, + "impressionId":{ + "type":"integer" + } + } + }, + "Vertex":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + }, + "serviceName":{ + "type":"string" + }, + "columnName":{ + "type":"string" + }, + "timestamp":{ + "type":"integer" + }, + "props":{ + "type":"object" + } + } + }, + "Step":{ + "type":"object", + "properties":{ + "label":{ + "type":"string" + }, + "direction":{ + "type":"string" + }, + "offset":{ + "type":"integer" + }, + "limit":{ + "type":"integer" + }, + "duplicate":{ + "type":"string" + } + } + }, + "Edge":{ + "type":"object", + "properties":{ + "from":{ + "type":"string" + }, + "to":{ + "type":"string" + }, + "direction":{ + "type":"string" + }, + "label":{ + "type":"string" + }, + "score":{ + "type":"string" + }, + "cacheRemain":{ + "type":"string" + }, + "_timestamp":{ + "type":"string" + }, + "timestamp":{ + "type":"string" + }, + "props":{ + "$ref":"#/definitions/EdgeProperties" + } + } + }, + "EdgeProperties":{ + "type":"object", + "properties":{ + "_timestamp":{ + "type":"integer" + }, + "time":{ + "type":"integer" + }, + "weight":{ + "type":"integer" + }, + "is_hidden":{ + "type":"boolean" + }, + "is_blocked":{ + "type":"boolean" + } + } + } + }, + "externalDocs":{ + "description":"Find out more about Apache S2Graph", + "url":"http://s2graph.incubator.apache.org/" + } +} diff --git a/s2http/src/main/scala/org/apache/s2graph/http/Server.scala b/s2http/src/main/scala/org/apache/s2graph/http/Server.scala index e70a1829..fb276f89 100644 --- a/s2http/src/main/scala/org/apache/s2graph/http/Server.scala +++ b/s2http/src/main/scala/org/apache/s2graph/http/Server.scala @@ -53,11 +53,15 @@ object Server extends App val port = sys.props.get("http.port").fold(8000)(_.toInt) val interface = sys.props.get("http.interface").fold("0.0.0.0")(identity) + val SwaggerIndexPage = "index.html" + val SwaggerIndexResPath = s"META-INF/resources/s2http/swagger/$SwaggerIndexPage" + val SwaggerWebJarBaseResPath = "META-INF/resources/webjars/swagger-ui/3.20.9/" + val startAt = System.currentTimeMillis() def uptime = System.currentTimeMillis() - startAt - def serverHealth = s"""{ "port": ${port}, "interface": "${interface}", "started_at": ${Instant.ofEpochMilli(startAt)}, "uptime": "${uptime} millis" """ + def serverHealth = s"""{ "port": ${port}, "interface": "${interface}", "started_at": "${Instant.ofEpochMilli(startAt)}", "uptime": "${uptime} millis" }""" def health = HttpResponse(status = StatusCodes.OK, entity = HttpEntity(ContentTypes.`application/json`, serverHealth)) @@ -67,6 +71,20 @@ object Server extends App pathPrefix("mutate")(mutateRoute), pathPrefix("admin")(adminRoute), pathPrefix("graphql")(graphqlRoute), + pathPrefix("api-docs") { + pathEnd { + redirect("/api-docs/", StatusCodes.TemporaryRedirect) + } ~ + path(Segments) { segs => { + val resPath = segs match { + case Nil | SwaggerIndexPage :: Nil => SwaggerIndexResPath + case "s2http" :: rest => "META-INF/resources/s2http/" + rest.mkString("/") + case _ => SwaggerWebJarBaseResPath + segs.mkString("/") + } + getFromResource(resPath) + } + } + }, get(complete(health)) )