Skip to content

Commit

Permalink
add option for manual routing
Browse files Browse the repository at this point in the history
  • Loading branch information
SMILEY4 committed Apr 25, 2024
1 parent 1cb9667 commit a9949d4
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 52 deletions.
85 changes: 33 additions & 52 deletions src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,30 @@
package io.github.smiley4.ktorswaggerui

import com.fasterxml.jackson.databind.ObjectMapper
import io.github.smiley4.ktorswaggerui.data.PluginConfigData
import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl
import io.github.smiley4.ktorswaggerui.routing.ForwardRouteController
import io.github.smiley4.ktorswaggerui.routing.SwaggerController
import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext
import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ComponentsBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.OpenApiBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.PathBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.PathsBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder
import io.github.smiley4.ktorswaggerui.builder.openapi.*
import io.github.smiley4.ktorswaggerui.builder.route.RouteCollector
import io.github.smiley4.ktorswaggerui.builder.route.RouteDocumentationMerger
import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta
import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder
import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext
import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder
import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationStarted
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.MonitoringEvent
import io.ktor.server.application.install
import io.ktor.server.application.plugin
import io.ktor.server.application.pluginOrNull
import io.ktor.server.routing.Routing
import io.ktor.server.webjars.Webjars
import io.github.smiley4.ktorswaggerui.data.PluginConfigData
import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl
import io.github.smiley4.ktorswaggerui.routing.ApiSpec
import io.github.smiley4.ktorswaggerui.routing.ForwardRouteController
import io.github.smiley4.ktorswaggerui.routing.SwaggerController
import io.ktor.server.application.*
import io.ktor.server.application.hooks.*
import io.ktor.server.routing.*
import io.ktor.server.webjars.*
import io.swagger.v3.core.util.Json
import mu.KotlinLogging
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set

/**
* This version must match the version of the gradle dependency
Expand All @@ -66,27 +43,27 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration
application.install(Webjars)
}

val apiSpecsJson = mutableMapOf<String, String>()
try {
val routes = routes(application, config)
apiSpecsJson.putAll(buildOpenApiSpecs(config, routes))
ApiSpec.setAll(buildOpenApiSpecs(config, routes))
} catch (e: Exception) {
logger.error("Error during application startup in swagger-ui-plugin", e)
}

apiSpecsJson.forEach { (specId, json) ->
val specConfig = config.specConfigs[specId] ?: config
SwaggerController(
applicationConfig!!,
specConfig,
SWAGGER_UI_WEBJARS_VERSION,
if (apiSpecsJson.size > 1) specId else null,
json
).setup(application)
}

if (apiSpecsJson.size == 1 && config.swaggerUI.forwardRoot) {
ForwardRouteController(applicationConfig!!, config).setup(application)
if (config.swaggerUI.automaticRouter) {
ApiSpec.getAll().forEach { (specId, json) ->
val specConfig = config.specConfigs[specId] ?: config
SwaggerController(
applicationConfig!!,
specConfig,
SWAGGER_UI_WEBJARS_VERSION,
if (ApiSpec.getAll().size > 1) specId else null,
json
).setup(application)
if (ApiSpec.getAll().size == 1 && config.swaggerUI.forwardRoot) {
ForwardRouteController(applicationConfig!!, config).setup(application)
}
}
}

}
Expand Down Expand Up @@ -146,7 +123,11 @@ private fun exampleContext(config: PluginConfigData, routes: List<RouteMeta>): E
).build(routes.toList())
}

private fun builder(config: PluginConfigData, schemaContext: SchemaContext, exampleContext: ExampleContext): OpenApiBuilder {
private fun builder(
config: PluginConfigData,
schemaContext: SchemaContext,
exampleContext: ExampleContext
): OpenApiBuilder {
return OpenApiBuilder(
config = config,
schemaContext = schemaContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.smiley4.ktorswaggerui.data

data class SwaggerUIData(
val automaticRouter: Boolean,
val forwardRoot: Boolean,
val swaggerUrl: String,
val rootHostPath: String,
Expand All @@ -15,6 +16,7 @@ data class SwaggerUIData(

companion object {
val DEFAULT = SwaggerUIData(
automaticRouter = true,
forwardRoot = false,
swaggerUrl = "swagger-ui",
rootHostPath = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ fun Route.documentation(
// ROUTING //
//============================//

fun Route.route(
builder: OpenApiRoute.() -> Unit = { },
build: Route.() -> Unit
): Route {
return documentation(builder) { route("", build) }
}

fun Route.route(
method: HttpMethod,
builder: OpenApiRoute.() -> Unit = { },
build: Route.() -> Unit
): Route {
return documentation(builder) { route("", method, build) }
}

fun Route.route(
path: String,
builder: OpenApiRoute.() -> Unit = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import io.github.smiley4.ktorswaggerui.data.SwaggerUIData
@OpenApiDslMarker
class SwaggerUIDsl {

/**
* Whether to use the automatic swagger-ui router or create swagger-ui router manually.
* 'false' results in [forwardRoot], [swaggerUrl], [rootHostPath], [authentication] being ignored.
*/
var automaticRouter: Boolean = SwaggerUIData.DEFAULT.automaticRouter

/**
* Whether to forward the root-url to the swagger-url
*/
Expand Down Expand Up @@ -87,6 +93,7 @@ class SwaggerUIDsl {

internal fun build(base: SwaggerUIData): SwaggerUIData {
return SwaggerUIData(
automaticRouter = automaticRouter,
forwardRoot = mergeBoolean(base.forwardRoot, this.forwardRoot),
swaggerUrl = mergeDefault(base.swaggerUrl, this.swaggerUrl, SwaggerUIData.DEFAULT.swaggerUrl),
rootHostPath = mergeDefault(base.rootHostPath, this.rootHostPath, SwaggerUIData.DEFAULT.rootHostPath),
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.smiley4.ktorswaggerui.routing

object ApiSpec {

private val apiSpecs = mutableMapOf<String, String>()

fun setAll(specs: Map<String, String>) {
apiSpecs.clear()
apiSpecs.putAll(specs)
}

fun set(name: String, spec: String) {
apiSpecs[name] = spec
}

fun get(name: String): String {
return apiSpecs[name] ?: throw NoSuchElementException("No api-spec with name $name registered.")
}

fun getAll(): Map<String, String> {
return apiSpecs
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.github.smiley4.ktorswaggerui.routing

import io.github.smiley4.ktorswaggerui.SWAGGER_UI_WEBJARS_VERSION
import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.data.SwaggerUIData
import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort
import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl
import io.github.smiley4.ktorswaggerui.dsl.route
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*


fun Route.openApiSpec(specId: String = PluginConfigDsl.DEFAULT_SPEC_ID) {
route({ hidden = true }) {
get {
call.respondText(ContentType.Application.Json, HttpStatusCode.OK) { ApiSpec.get(specId) }
}
}
}

fun Route.swaggerUI(apiUrl: String) {
route({ hidden = true }) {
get {
call.respondRedirect("${call.request.uri}/index.html")
}
get("{filename}") {
serveStaticResource(call.parameters["filename"]!!, SWAGGER_UI_WEBJARS_VERSION, call)
}
get("swagger-initializer.js") {
serveSwaggerInitializer(call, SwaggerUIData.DEFAULT, apiUrl)
}
}
}

private suspend fun serveSwaggerInitializer(call: ApplicationCall, swaggerUiConfig: SwaggerUIData, apiUrl: String) {
// see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md for reference
val propValidatorUrl = swaggerUiConfig.validatorUrl?.let { "validatorUrl: \"$it\"" } ?: "validatorUrl: false"
val propDisplayOperationId = "displayOperationId: ${swaggerUiConfig.displayOperationId}"
val propFilter = "filter: ${swaggerUiConfig.showTagFilterInput}"
val propSort = "operationsSorter: " +
if (swaggerUiConfig.sort == SwaggerUiSort.NONE) "undefined"
else "\"${swaggerUiConfig.sort.value}\""
val propSyntaxHighlight = "syntaxHighlight: { theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }"
val content = """
window.onload = function() {
window.ui = SwaggerUIBundle({
url: "$apiUrl",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
withCredentials: ${swaggerUiConfig.withCredentials},
$propValidatorUrl,
$propDisplayOperationId,
$propFilter,
$propSort,
$propSyntaxHighlight
});
};
""".trimIndent()
call.respondText(ContentType.Application.JavaScript, HttpStatusCode.OK) { content }
}

private suspend fun serveStaticResource(filename: String, swaggerWebjarVersion: String, call: ApplicationCall) {
val resourceName = "/META-INF/resources/webjars/swagger-ui/$swaggerWebjarVersion/$filename"
val resource = SwaggerUI::class.java.getResource(resourceName)
if (resource != null) {
call.respond(ResourceContent(resource))
} else {
call.respond(HttpStatusCode.NotFound, "$filename could not be found")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.smiley4.ktorswaggerui.examples

import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.routing.openApiSpec
import io.github.smiley4.ktorswaggerui.routing.swaggerUI
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

/**
* An example showcasing manual swaggerui-routing
*/
fun main() {
embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true)
}

private fun Application.myModule() {

install(SwaggerUI) {
swagger {
automaticRouter = false
}
}

routing {

route("swagger") {
swaggerUI("/api.json")
}
route("api.json") {
openApiSpec()
}

get("hello", {
description = "Simple 'Hello World'- Route"
}) {
call.respondText("Hello World!")
}
}
}
Loading

0 comments on commit a9949d4

Please sign in to comment.