Repository: bkbnio/kompendium
Pull request #426 (2023-03-02 18:29:27)

reintroduce swagger compatability
# Description

Reintroduce swagger functionality that existed in v2.3.5 but was removed in v3.0.0. See [Swagger.kt@2.3.5](https://github.com/bkbnio/kompendium/blob/v2.3.5/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/routes/Swagger.kt)

Closes # (issue)

[425](https://github.com/bkbnio/kompendium/issues/425)

## Type of change

Please delete options that are not relevant.

- [ ] New feature (non-breaking change which adds functionality)


# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have updated the CHANGELOG in the `Unreleased` section
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules


unredundant: ![7402d430f81ac317112af5ccf8adda13](https://user-images.githubusercontent.com/5607577/222526385-a04c761b-087d-48a3-bba7-282f8b851a75.jpg)

I get this when I try to load the basic playground example at `localhost:8081/swagger-ui`
Kulikovpavel: Yes, you are right

It seems that it was in 4.16.0-alpha3, but for some reason it was not included in 4.16.0 and even 4.17.0, which was released yesterday
I will discuss this with the swagger-ui team
Kulikovpavel: They'll introduce it in next major version, 5.x
https://github.com/swagger-api/swagger-ui/issues/8441
unredundant: Cya in 2026 🫡
Kulikovpavel: @unredundant future is here, they've released 5.0.0-alpha.0 version, my PR is working with it
<img width="663" alt="image" src="https://user-images.githubusercontent.com/1457767/225102849-991a27c7-3fa2-47c3-b46a-6b132ad7398f.png">

unredundant: Nice! Out of curiosity... is there a way to just get the latest swagger ui version? Would be nice if the default could just be the most recent version, and have users override if they want to change it.

Otherwise, we'll need to keep pushing PRs to bump the default version of the swagger client.
Kulikovpavel: I think it will work for any latest version without @version addition in html template
So https://unpkg.com/swagger-ui-dist/index.js points to 4.18.1
Let me change PR a bit to support future releases by default, but keep opportunity to provide version

So with `swagger(pageTitle = "Simple API Docs", swaggerVersion="5.0.0-alpha.0")` it will work now, without version - in the bright future after swagger-ui release
unredundant: Awesome 👍 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f0ede238..0e00785b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 ### Added
 
+- Add support for swagger documentation
+
 ### Changed
 
 ### Remove
diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Redoc.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Redoc.kt
index ca6bcd3d5..2523d6a89 100644
--- a/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Redoc.kt
+++ b/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Redoc.kt
@@ -18,7 +18,7 @@ import kotlinx.html.unsafe
 /**
  * Provides an out-of-the-box route to view docs using ReDoc on the specified [path].
  * @param pageTitle Webpage title you wish to be displayed on your docs
- * @param route path to docs resource
+ * @param path path to docs resource
  * @param specUrl url to point ReDoc to the OpenAPI json document
  */
 fun Route.redoc(pageTitle: String = "Docs", path: String = "/docs", specUrl: String = "/openapi.json") {
diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Swagger.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Swagger.kt
new file mode 100644
index 000000000..70a1ec94c
--- /dev/null
+++ b/core/src/main/kotlin/io/bkbn/kompendium/core/routes/Swagger.kt
@@ -0,0 +1,91 @@
+package io.bkbn.kompendium.core.routes
+
+import io.ktor.server.application.call
+import io.ktor.server.html.respondHtml
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.get
+import io.ktor.server.routing.route
+import kotlinx.html.body
+import kotlinx.html.div
+import kotlinx.html.head
+import kotlinx.html.id
+import kotlinx.html.link
+import kotlinx.html.meta
+import kotlinx.html.script
+import kotlinx.html.title
+import kotlinx.html.unsafe
+
+/**
+ * Provides an out-of-the-box route to view docs using Swagger
+ * @see <a href="https://swagger.io/specification/">Swagger OpenApi Specification</a>
+ * for the latest supported open api version.
+ * @param pageTitle Webpage title you wish to be displayed on your docs
+ * @param path path to docs resource
+ * @param specUrl url to point Swagger to the OpenAPI json document
+ * @param swaggerVersion version of swagger-ui distribution
+ */
+fun Route.swagger(
+  pageTitle: String = "Docs",
+  path: String = "/swagger-ui",
+  specUrl: String = "/openapi.json",
+  swaggerVersion: String? = null
+) {
+  val swaggerVersionSuffix = if (swaggerVersion == null) "" else "@$swaggerVersion"
+
+  route(path) {
+    get {
+      call.respondHtml {
+        head {
+          title {
+            +pageTitle
+          }
+          meta {
+            charset = "utf-8"
+          }
+          meta {
+            name = "viewport"
+            content = "width=device-width, initial-scale=1"
+          }
+          link {
+            href = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui.css"
+            rel = "stylesheet"
+          }
+        }
+        body {
+          div {
+            id = "swagger-ui"
+          }
+          script {
+            src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-standalone-preset.js"
+          }
+          script {
+            src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-bundle.js"
+          }
+          unsafe {
+            +"""
+              <script>
+                window.onload = function () {
+                  // Build a system
+                  const ui = SwaggerUIBundle({
+                    url: "$specUrl",
+                    dom_id: '#swagger-ui',
+                    deepLinking: true,
+                    presets: [
+                      SwaggerUIBundle.presets.apis,
+                      SwaggerUIStandalonePreset
+                    ],
+                    plugins: [
+                      SwaggerUIBundle.plugins.DownloadUrl
+                    ],
+                    layout: "StandaloneLayout",
+                  })
+                  window.ui = ui
+                }
+              </script>
+            """.trimIndent()
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt
index 83784e32f..96fe6d72a 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt
@@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.component.Components
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -68,6 +69,7 @@ private fun Application.mainModule() {
     )
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
     authenticate("basic") {
       route("/{id}") {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt
index 78fa65414..ed1d240d5 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt
@@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -49,6 +50,7 @@ private fun Application.mainModule() {
     schemaConfigurator = KotlinXSchemaConfigurator()
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
     route("/{id}") {
       idDocumentation()
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt
index 4e53b9654..c122d6f23 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt
@@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
 import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
@@ -51,6 +52,7 @@ private fun Application.mainModule() {
     )
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
 
     route("/{id}") {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt
index fb9021890..c8aee894b 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/EnrichmentPlayground.kt
@@ -5,6 +5,7 @@ import io.bkbn.kompendium.core.metadata.PostInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.enrichment.TypeEnrichment
 import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
@@ -50,6 +51,7 @@ private fun Application.mainModule() {
     schemaConfigurator = KotlinXSchemaConfigurator()
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
     enrichedDocumentation()
     post {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt
index 2ba34c939..599133e2b 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt
@@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
 import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
@@ -50,6 +51,7 @@ private fun Application.mainModule() {
     }
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
 
     route("/{id}") {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt
index e88ff5d66..54dfae6f3 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt
@@ -6,6 +6,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.SchemaConfigurator
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -48,6 +49,7 @@ private fun Application.mainModule() {
     schemaConfigurator = GsonSchemaConfigurator()
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
 
     route("/{id}") {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt
index 9bab93fcd..b2de57d23 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt
@@ -5,6 +5,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.component.Components
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -80,6 +81,7 @@ private fun Application.mainModule() {
   }
   routing {
     authenticate("basic") {
+      swagger(pageTitle = "Simple API Docs")
       redoc(pageTitle = "Simple API Docs")
       route("/{id}") {
         locationDocumentation()
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt
index b93d3c29f..ffb847eb3 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt
@@ -8,6 +8,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.plugin.NotarizedRoute
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.SchemaConfigurator
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -51,6 +52,7 @@ private fun Application.mainModule() {
     schemaConfigurator = JacksonSchemaConfigurator()
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
 
     route("/{id}") {
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
index a6e59ba2b..8eb0838f8 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
@@ -3,6 +3,7 @@ package io.bkbn.kompendium.playground
 import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.locations.NotarizedLocations
 import io.bkbn.kompendium.oas.payload.Parameter
@@ -72,6 +73,7 @@ private fun Application.mainModule() {
     )
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
     get<Listing> { listing ->
       call.respondText("Listing ${listing.name}, page ${listing.page}")
diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt
index 0108002b2..1119537d6 100644
--- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt
+++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ResourcesPlayground.kt
@@ -3,6 +3,7 @@ package io.bkbn.kompendium.playground
 import io.bkbn.kompendium.core.metadata.GetInfo
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.definition.TypeDefinition
 import io.bkbn.kompendium.oas.payload.Parameter
 import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
@@ -73,6 +74,7 @@ private fun Application.mainModule() {
     )
   }
   routing {
+    swagger(pageTitle = "Simple API Docs")
     redoc(pageTitle = "Simple API Docs")
     get<ListingResource> { listing ->
       call.respondText("Listing ${listing.name}, page ${listing.page}")


Test patch:
diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt
index 30ab58f6a..e59c1ee73 100644
--- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt
+++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.SerializationFeature
 import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
 import io.bkbn.kompendium.core.plugin.NotarizedApplication
 import io.bkbn.kompendium.core.routes.redoc
+import io.bkbn.kompendium.core.routes.swagger
 import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
 import io.bkbn.kompendium.json.schema.definition.JsonSchema
 import io.bkbn.kompendium.oas.OpenApiSpec
@@ -130,6 +131,7 @@ object TestHelpers {
     }
     application(applicationSetup)
     routing {
+      swagger()
       redoc()
       routeUnderTest()
     }