Feature/swagger UI (#34)
This commit is contained in:
14
.github/workflows/pr_checks.yml
vendored
14
.github/workflows/pr_checks.yml
vendored
@ -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:
|
||||
|
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -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:
|
||||
|
11
CHANGELOG.md
11
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
|
||||
|
16
README.md
16
README.md
@ -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
|
||||
|
||||
|
@ -25,7 +25,7 @@ allprojects {
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "14"
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=0.6.0
|
||||
project.version=0.6.1
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -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" ]
|
||||
|
@ -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> {
|
||||
|
@ -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].
|
||||
|
@ -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",
|
||||
|
@ -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" : [ ]
|
||||
}
|
@ -8,6 +8,7 @@ dependencies {
|
||||
|
||||
implementation(projects.kompendiumCore)
|
||||
implementation(projects.kompendiumAuth)
|
||||
implementation(projects.kompendiumSwaggerUi)
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.bundles.ktorAuth)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
kompendium-swagger-ui/build.gradle.kts
Normal file
39
kompendium-swagger-ui/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
rootProject.name = "kompendium"
|
||||
include("kompendium-core")
|
||||
include("kompendium-auth")
|
||||
include("kompendium-swagger-ui")
|
||||
include("kompendium-playground")
|
||||
|
||||
// Feature Previews
|
||||
|
Reference in New Issue
Block a user