Feature/swagger UI (#34)

This commit is contained in:
dpnolte
2021-04-23 14:44:26 +02:00
committed by GitHub
parent d0767aa74e
commit ea09aa72e2
16 changed files with 199 additions and 40 deletions

View File

@ -5,10 +5,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 1.14 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v2
with: with:
java-version: 1.14 distribution: 'adopt'
java-version: '11'
- name: Cache Gradle packages - name: Cache Gradle packages
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
@ -21,10 +22,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 1.14 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v2
with: with:
java-version: 1.14 distribution: 'adopt'
java-version: '11'
- name: Cache Gradle packages - name: Cache Gradle packages
uses: actions/cache@v2 uses: actions/cache@v2
with: with:

View File

@ -7,9 +7,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.14 distribution: 'adopt'
java-version: '11'
- name: Cache Gradle packages - name: Cache Gradle packages
uses: actions/cache@v2 uses: actions/cache@v2
with: with:

View File

@ -1,5 +1,16 @@
# Changelog # Changelog
## [0.6.1] - April 23rd, 2021
### Added
- Added support for Swagger ui
### Changed
- Set jvm target to 11
- Resolved bug for empty params and/or empty response body
## [0.6.0] - April 21st, 2021 ## [0.6.0] - April 21st, 2021
### Added ### Added

View File

@ -146,11 +146,11 @@ A minimal example would be:
install(Authentication) { install(Authentication) {
notarizedBasic("basic") { notarizedBasic("basic") {
realm = "Ktor realm 1" realm = "Ktor realm 1"
// ... // configure basic authentication provider..
} }
notarizedJwt("jwt") { notarizedJwt("jwt") {
realm = "Ktor realm 2" realm = "Ktor realm 2"
// ... // configure jwt authentication provider...
} }
} }
routing { routing {
@ -181,7 +181,17 @@ A minimal example would be:
} }
``` ```
### Enabling Swagger ui
To enable Swagger UI, `kompendium-swagger-ui` needs to be added.
This will also add the [ktor webjars feature](https://ktor.io/docs/webjars.html) to your classpath as it is required for swagger ui.
Minimal Example:
```kotlin
install(Webjars)
routing {
openApi()
swaggerUI()
}
```
## Limitations ## Limitations

View File

@ -25,7 +25,7 @@ allprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "14" jvmTarget = "11"
} }
} }

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=0.6.0 project.version=0.6.1
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle

View File

@ -3,6 +3,7 @@ kotlin = "1.4.32"
ktor = "1.5.3" ktor = "1.5.3"
slf4j = "1.7.30" slf4j = "1.7.30"
logback = "1.2.3" logback = "1.2.3"
swagger-ui = "3.47.1"
[libraries] [libraries]
# API # API
@ -12,12 +13,16 @@ ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor"
ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" } ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" }
ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" } ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" }
ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" } ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" }
ktor-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" }
# Logging # Logging
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" } logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
# webjars
webjars-swagger-ui = { group "org.webjars", name = "swagger-ui", version.ref = "swagger-ui" }
[bundles] [bundles]
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ] ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ] ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ]

View File

@ -33,6 +33,7 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.path.CorePathCalculator import org.leafygreens.kompendium.path.CorePathCalculator
import org.leafygreens.kompendium.path.PathCalculator import org.leafygreens.kompendium.path.PathCalculator
import org.leafygreens.kompendium.util.Helpers
import org.leafygreens.kompendium.util.Helpers.getReferenceSlug import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
object Kompendium { object Kompendium {
@ -123,37 +124,36 @@ object Kompendium {
} }
// TODO These two lookin' real similar 👀 Combine? // TODO These two lookin' real similar 👀 Combine?
private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (this) { private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (requestInfo) {
Unit::class -> null
else -> when (requestInfo) {
null -> null null -> null
else -> OpenApiSpecRequest( else -> {
OpenApiSpecRequest(
description = requestInfo.description, description = requestInfo.description,
content = requestInfo.mediaTypes.associateWith { content = resolveContent(requestInfo.mediaTypes) ?: mapOf()
val ref = getReferenceSlug()
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
}
) )
} }
} }
private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (this) { private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (responseInfo) {
Unit::class -> null // TODO Maybe not though? could be unit but 200 🤔
else -> when (responseInfo) {
null -> null // TODO again probably revisit this null -> null // TODO again probably revisit this
else -> { else -> {
val content = responseInfo.mediaTypes.associateWith {
val ref = getReferenceSlug()
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
}
val specResponse = OpenApiSpecResponse( val specResponse = OpenApiSpecResponse(
description = responseInfo.description, description = responseInfo.description,
content = content.ifEmpty { null } content = resolveContent(responseInfo.mediaTypes)
) )
Pair(responseInfo.status, specResponse) Pair(responseInfo.status, specResponse)
} }
} }
private fun KType.resolveContent(mediaTypes: List<String>): Map<String, OpenApiSpecMediaType>? {
return if (this != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith {
val ref = getReferenceSlug()
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
} }
} else null
}
// TODO God these annotations make this hideous... any way to improve? // TODO God these annotations make this hideous... any way to improve?
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> { private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {

View File

@ -11,6 +11,7 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import kotlin.reflect.full.createType
object Helpers { object Helpers {
@ -18,6 +19,7 @@ object Helpers {
const val COMPONENT_SLUG = "#/components/schemas" const val COMPONENT_SLUG = "#/components/schemas"
val UNIT_TYPE by lazy { Unit::class.createType() }
/** /**
* Simple extension function that will take a [Pair] and place it (if absent) into a [MutableMap]. * Simple extension function that will take a [Pair] and place it (if absent) into a [MutableMap].

View File

@ -324,6 +324,22 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can notarize route with no request params and no response body`() {
withTestApplication({
configModule()
docs()
emptyGet()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("no_request_params_and_no_response_body.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test @Test
fun `Generates the expected redoc`() { fun `Generates the expected redoc`() {
withTestApplication({ withTestApplication({
@ -351,6 +367,7 @@ internal class KompendiumTest {
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!", testPostResponse, testRequest) val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!", testPostResponse, testRequest)
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!", testPostResponse, testRequest) val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!", testPostResponse, testRequest)
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes", testDeleteResponse) val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes", testDeleteResponse)
val emptyTestGetInfo = MethodInfo("No request params and response body", "testing more")
} }
private fun Application.configModule() { private fun Application.configModule() {
@ -486,6 +503,16 @@ internal class KompendiumTest {
} }
} }
private fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet<Unit, Unit>(emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private val oas = Kompendium.openApiSpec.copy( private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo( info = OpenApiSpecInfo(
title = "Test API", title = "Test API",

View File

@ -0,0 +1,42 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish \uD83D\uDE09 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/lg-backbone/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/empty" : {
"get" : {
"tags" : [ ],
"summary" : "No request params and response body",
"description" : "testing more",
"parameters" : [ ],
"deprecated" : false
}
}
},
"components" : {
"schemas" : { },
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -8,6 +8,7 @@ dependencies {
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation(projects.kompendiumAuth) implementation(projects.kompendiumAuth)
implementation(projects.kompendiumSwaggerUi)
implementation(libs.bundles.ktor) implementation(libs.bundles.ktor)
implementation(libs.bundles.ktorAuth) implementation(libs.bundles.ktorAuth)

View File

@ -9,12 +9,15 @@ import io.ktor.auth.Authentication
import io.ktor.auth.authenticate import io.ktor.auth.authenticate
import io.ktor.auth.UserIdPrincipal import io.ktor.auth.UserIdPrincipal
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.route import io.ktor.routing.route
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.webjars.Webjars
import java.net.URI import java.net.URI
import org.leafygreens.kompendium.Kompendium import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Kompendium.notarizedDelete import org.leafygreens.kompendium.Kompendium.notarizedDelete
@ -40,6 +43,7 @@ import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePostInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePutInfo import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePutInfo
import org.leafygreens.kompendium.routes.openApi import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.swagger.swaggerUI
import org.leafygreens.kompendium.util.KompendiumHttpCodes import org.leafygreens.kompendium.util.KompendiumHttpCodes
private val oas = Kompendium.openApiSpec.copy( private val oas = Kompendium.openApiSpec.copy(
@ -100,11 +104,13 @@ fun Application.mainModule() {
} }
} }
} }
install(Webjars)
featuresInstalled = true featuresInstalled = true
} }
routing { routing {
openApi(oas) openApi(oas)
redoc(oas) redoc(oas)
swaggerUI()
route("/test") { route("/test") {
route("/{id}") { route("/{id}") {
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) { notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
@ -128,7 +134,7 @@ fun Application.mainModule() {
authenticate("basic") { authenticate("basic") {
route("/authenticated/single") { route("/authenticated/single") {
notarizedGet<Unit, Unit>(testAuthenticatedSingleGetInfo) { notarizedGet<Unit, Unit>(testAuthenticatedSingleGetInfo) {
call.respondText("get authentiticated single") call.respond(HttpStatusCode.OK)
} }
} }
} }

View File

@ -0,0 +1,39 @@
plugins {
`java-library`
`maven-publish`
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.bundles.ktor)
api(libs.ktor.webjars)
implementation(libs.webjars.swagger.ui)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
}
java {
withSourcesJar()
}
publishing {
repositories {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/lg-backbone/kompendium")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("kompendium") {
from(components["kotlin"])
artifact(tasks.sourcesJar)
}
}
}

View File

@ -0,0 +1,12 @@
package org.leafygreens.kompendium.swagger
import io.ktor.application.call
import io.ktor.response.respondRedirect
import io.ktor.routing.Routing
import io.ktor.routing.get
fun Routing.swaggerUI(openApiJsonUrl: String = "/openapi.json") {
get("/swagger-ui") {
call.respondRedirect("/webjars/swagger-ui/index.html?url=$openApiJsonUrl", true)
}
}

View File

@ -1,6 +1,7 @@
rootProject.name = "kompendium" rootProject.name = "kompendium"
include("kompendium-core") include("kompendium-core")
include("kompendium-auth") include("kompendium-auth")
include("kompendium-swagger-ui")
include("kompendium-playground") include("kompendium-playground")
// Feature Previews // Feature Previews