diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index acbd5b453..bdb5e99f2 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -5,10 +5,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.14 - uses: actions/setup-java@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.14 + distribution: 'adopt' + java-version: '11' - name: Cache Gradle packages uses: actions/cache@v2 with: @@ -21,10 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.14 - uses: actions/setup-java@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.14 + distribution: 'adopt' + java-version: '11' - name: Cache Gradle packages uses: actions/cache@v2 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a7f4bf55b..2f07165a2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,9 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: 1.14 + distribution: 'adopt' + java-version: '11' - name: Cache Gradle packages uses: actions/cache@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 22235f000..edfe7bab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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 ### Added diff --git a/README.md b/README.md index ecaec000e..26e359d75 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,13 @@ At the moment, the basic and jwt authentication is only supported. A minimal example would be: ```kotlin install(Authentication) { - notarizedBasic("basic") { - realm = "Ktor realm 1" - // ... + notarizedBasic("basic") { + realm = "Ktor realm 1" + // configure basic authentication provider.. } notarizedJwt("jwt") { realm = "Ktor realm 2" - // ... + // configure jwt authentication provider... } } 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 diff --git a/build.gradle.kts b/build.gradle.kts index e5d1c20f9..6245063f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ allprojects { tasks.withType().configureEach { kotlinOptions { - jvmTarget = "14" + jvmTarget = "11" } } diff --git a/gradle.properties b/gradle.properties index 29a7c3018..9f0275fdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=0.6.0 +project.version=0.6.1 # Kotlin kotlin.code.style=official # Gradle diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cc091145..2211fae21 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ kotlin = "1.4.32" ktor = "1.5.3" slf4j = "1.7.30" logback = "1.2.3" +swagger-ui = "3.47.1" [libraries] # 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-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-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" } # Logging slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", 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] ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ] ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ] diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt index 6352afcae..d554f2df6 100644 --- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt @@ -33,6 +33,7 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef import org.leafygreens.kompendium.path.CorePathCalculator import org.leafygreens.kompendium.path.PathCalculator +import org.leafygreens.kompendium.util.Helpers import org.leafygreens.kompendium.util.Helpers.getReferenceSlug object Kompendium { @@ -123,38 +124,37 @@ object Kompendium { } // TODO These two lookin' real similar 👀 Combine? - private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (this) { - Unit::class -> null - else -> when (requestInfo) { - null -> null - else -> OpenApiSpecRequest( + private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (requestInfo) { + null -> null + else -> { + OpenApiSpecRequest( description = requestInfo.description, - content = requestInfo.mediaTypes.associateWith { - val ref = getReferenceSlug() - OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref)) - } + content = resolveContent(requestInfo.mediaTypes) ?: mapOf() ) } } - private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair? = when (this) { - Unit::class -> null // TODO Maybe not though? could be unit but 200 🤔 - else -> when (responseInfo) { - null -> null // TODO again probably revisit this - else -> { - val content = responseInfo.mediaTypes.associateWith { - val ref = getReferenceSlug() - OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref)) - } - val specResponse = OpenApiSpecResponse( - description = responseInfo.description, - content = content.ifEmpty { null } - ) - Pair(responseInfo.status, specResponse) - } + private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair? = when (responseInfo) { + null -> null // TODO again probably revisit this + else -> { + val specResponse = OpenApiSpecResponse( + description = responseInfo.description, + content = resolveContent(responseInfo.mediaTypes) + ) + Pair(responseInfo.status, specResponse) } } + private fun KType.resolveContent(mediaTypes: List): Map? { + 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? private fun KType.toParameterSpec(): List { val clazz = classifier as KClass<*> diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/util/Helpers.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/util/Helpers.kt index 1fe4ed643..f17f17de1 100644 --- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/util/Helpers.kt +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/util/Helpers.kt @@ -11,6 +11,7 @@ import kotlin.reflect.KProperty import kotlin.reflect.KType import kotlin.reflect.jvm.javaField import org.slf4j.LoggerFactory +import kotlin.reflect.full.createType object Helpers { @@ -18,6 +19,7 @@ object Helpers { 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]. diff --git a/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt b/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt index dddc60a97..d5f5699d6 100644 --- a/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt @@ -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 fun `Generates the expected redoc`() { withTestApplication({ @@ -351,6 +367,7 @@ internal class KompendiumTest { 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 testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes", testDeleteResponse) + val emptyTestGetInfo = MethodInfo("No request params and response body", "testing more") } private fun Application.configModule() { @@ -486,6 +503,16 @@ internal class KompendiumTest { } } + private fun Application.emptyGet() { + routing { + route("/test/empty") { + notarizedGet(emptyTestGetInfo) { + call.respond(HttpStatusCode.OK) + } + } + } + } + private val oas = Kompendium.openApiSpec.copy( info = OpenApiSpecInfo( title = "Test API", diff --git a/kompendium-core/src/test/resources/no_request_params_and_no_response_body.json b/kompendium-core/src/test/resources/no_request_params_and_no_response_body.json new file mode 100644 index 000000000..e83294219 --- /dev/null +++ b/kompendium-core/src/test/resources/no_request_params_and_no_response_body.json @@ -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" : [ ] +} diff --git a/kompendium-playground/build.gradle.kts b/kompendium-playground/build.gradle.kts index e52e5bc5f..96c3ba691 100644 --- a/kompendium-playground/build.gradle.kts +++ b/kompendium-playground/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(projects.kompendiumCore) implementation(projects.kompendiumAuth) + implementation(projects.kompendiumSwaggerUi) implementation(libs.bundles.ktor) implementation(libs.bundles.ktorAuth) diff --git a/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt b/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt index 81305086b..868b4171b 100644 --- a/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt +++ b/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt @@ -9,12 +9,15 @@ import io.ktor.auth.Authentication import io.ktor.auth.authenticate import io.ktor.auth.UserIdPrincipal import io.ktor.features.ContentNegotiation +import io.ktor.http.HttpStatusCode import io.ktor.jackson.jackson +import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.route import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import io.ktor.webjars.Webjars import java.net.URI import org.leafygreens.kompendium.Kompendium 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.routes.openApi import org.leafygreens.kompendium.routes.redoc +import org.leafygreens.kompendium.swagger.swaggerUI import org.leafygreens.kompendium.util.KompendiumHttpCodes private val oas = Kompendium.openApiSpec.copy( @@ -100,11 +104,13 @@ fun Application.mainModule() { } } } + install(Webjars) featuresInstalled = true } routing { openApi(oas) redoc(oas) + swaggerUI() route("/test") { route("/{id}") { notarizedGet(testIdGetInfo) { @@ -128,7 +134,7 @@ fun Application.mainModule() { authenticate("basic") { route("/authenticated/single") { notarizedGet(testAuthenticatedSingleGetInfo) { - call.respondText("get authentiticated single") + call.respond(HttpStatusCode.OK) } } } diff --git a/kompendium-swagger-ui/build.gradle.kts b/kompendium-swagger-ui/build.gradle.kts new file mode 100644 index 000000000..25ac6f861 --- /dev/null +++ b/kompendium-swagger-ui/build.gradle.kts @@ -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("kompendium") { + from(components["kotlin"]) + artifact(tasks.sourcesJar) + } + } +} diff --git a/kompendium-swagger-ui/src/main/kotlin/org/leafygreens/kompendium/swagger/SwaggerUI.kt b/kompendium-swagger-ui/src/main/kotlin/org/leafygreens/kompendium/swagger/SwaggerUI.kt new file mode 100644 index 000000000..19fc722b4 --- /dev/null +++ b/kompendium-swagger-ui/src/main/kotlin/org/leafygreens/kompendium/swagger/SwaggerUI.kt @@ -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) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dd24d7ca9..548a4360a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "kompendium" include("kompendium-core") include("kompendium-auth") +include("kompendium-swagger-ui") include("kompendium-playground") // Feature Previews