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
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:

View File

@ -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:

View File

@ -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

View File

@ -146,11 +146,11 @@ A minimal example would be:
install(Authentication) {
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

View File

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

View File

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

View File

@ -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" ]

View File

@ -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,37 +124,36 @@ object Kompendium {
}
// TODO These two lookin' real similar 👀 Combine?
private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (this) {
Unit::class -> null
else -> when (requestInfo) {
private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (requestInfo) {
null -> null
else -> OpenApiSpecRequest(
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<Int, OpenApiSpecResponse>? = when (this) {
Unit::class -> null // TODO Maybe not though? could be unit but 200 🤔
else -> when (responseInfo) {
private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = 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 }
content = resolveContent(responseInfo.mediaTypes)
)
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?
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {

View File

@ -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].

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
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<Unit, Unit>(emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo(
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.kompendiumAuth)
implementation(projects.kompendiumSwaggerUi)
implementation(libs.bundles.ktor)
implementation(libs.bundles.ktorAuth)

View File

@ -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<ExampleParams, ExampleResponse>(testIdGetInfo) {
@ -128,7 +134,7 @@ fun Application.mainModule() {
authenticate("basic") {
route("/authenticated/single") {
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"
include("kompendium-core")
include("kompendium-auth")
include("kompendium-swagger-ui")
include("kompendium-playground")
// Feature Previews