redoc and openapi routes standardized (#28)

This commit is contained in:
Ryan Brink
2021-04-19 09:11:32 -04:00
committed by GitHub
parent ae4999483b
commit dd978276d2
7 changed files with 188 additions and 141 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## [0.5.0] - April 19th, 2021
### Added
- Expose `/openapi.json` and `/docs` as opt-in pre-built Routes
## [0.4.0] - April 17th, 2021
### Added

View File

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

View File

@ -0,0 +1,16 @@
package org.leafygreens.kompendium.routes
import io.ktor.application.call
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
import org.leafygreens.kompendium.models.oas.OpenApiSpec
fun Routing.openApi(oas: OpenApiSpec) {
route("/openapi.json") {
get {
call.respond(oas)
}
}
}

View File

@ -0,0 +1,53 @@
package org.leafygreens.kompendium.routes
import io.ktor.application.call
import io.ktor.html.respondHtml
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.link
import kotlinx.html.meta
import kotlinx.html.script
import kotlinx.html.style
import kotlinx.html.title
import kotlinx.html.unsafe
import org.leafygreens.kompendium.models.oas.OpenApiSpec
fun Routing.redoc(oas: OpenApiSpec) {
route("/docs") {
get {
call.respondHtml {
head {
title {
+"${oas.info.title}"
}
meta {
charset = "utf-8"
}
meta {
name = "viewport"
content = "width=device-width, initial-scale=1"
}
link {
href = "https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel = "stylesheet"
}
style {
unsafe {
raw("body { margin: 0; padding: 0; }")
}
}
}
body {
// TODO needs to mirror openApi route
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
script {
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
}
}
}
}
}
}

View File

@ -11,7 +11,6 @@ import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest
@ -31,11 +30,12 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.ComplexRequest
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import org.leafygreens.kompendium.util.TestCreatedResponse
import org.leafygreens.kompendium.util.TestData
import org.leafygreens.kompendium.util.TestDeleteResponse
import org.leafygreens.kompendium.util.TestParams
import org.leafygreens.kompendium.util.TestRequest
import org.leafygreens.kompendium.util.TestResponse
@ -56,7 +56,7 @@ internal class KompendiumTest {
fun `Notarized Get records all expected information`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedGetModule()
}) {
// do
@ -72,7 +72,7 @@ internal class KompendiumTest {
fun `Notarized Get does not interrupt the pipeline`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedGetModule()
}) {
// do
@ -88,7 +88,7 @@ internal class KompendiumTest {
fun `Notarized Post records all expected information`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedPostModule()
}) {
// do
@ -104,7 +104,7 @@ internal class KompendiumTest {
fun `Notarized post does not interrupt the pipeline`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedPostModule()
}) {
// do
@ -120,7 +120,7 @@ internal class KompendiumTest {
fun `Notarized Put records all expected information`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedPutModule()
}) {
// do
@ -137,7 +137,7 @@ internal class KompendiumTest {
fun `Notarized put does not interrupt the pipeline`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedPutModule()
}) {
// do
@ -153,7 +153,7 @@ internal class KompendiumTest {
fun `Notarized delete records all expected information`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedDeleteModule()
}) {
// do
@ -169,7 +169,7 @@ internal class KompendiumTest {
fun `Notarized delete does not interrupt the pipeline`() {
withTestApplication({
configModule()
openApiModule()
docs()
notarizedDeleteModule()
}) {
// do
@ -184,7 +184,7 @@ internal class KompendiumTest {
fun `Path parser stores the expected path`() {
withTestApplication({
configModule()
openApiModule()
docs()
pathParsingTestModule()
}) {
// do
@ -200,7 +200,7 @@ internal class KompendiumTest {
fun `Can notarize the root route`() {
withTestApplication({
configModule()
openApiModule()
docs()
rootModule()
}) {
// do
@ -216,7 +216,7 @@ internal class KompendiumTest {
fun `Can call the root route`() {
withTestApplication({
configModule()
openApiModule()
docs()
rootModule()
}) {
// do
@ -232,7 +232,7 @@ internal class KompendiumTest {
fun `Can notarize a trailing slash route`() {
withTestApplication({
configModule()
openApiModule()
docs()
trailingSlash()
}) {
// do
@ -248,7 +248,7 @@ internal class KompendiumTest {
fun `Can call a trailing slash route`() {
withTestApplication({
configModule()
openApiModule()
docs()
trailingSlash()
}) {
// do
@ -264,7 +264,7 @@ internal class KompendiumTest {
fun `Can notarize a complex type`() {
withTestApplication({
configModule()
openApiModule()
docs()
complexType()
}) {
// do
@ -280,7 +280,7 @@ internal class KompendiumTest {
fun `Can notarize primitives`() {
withTestApplication({
configModule()
openApiModule()
docs()
primitives()
}) {
// do
@ -296,7 +296,7 @@ internal class KompendiumTest {
fun `Can notarize a top level list response`() {
withTestApplication({
configModule()
openApiModule()
docs()
returnsList()
}) {
// do
@ -308,6 +308,23 @@ internal class KompendiumTest {
}
}
@Test
fun `Generates the expected redoc`() {
withTestApplication({
configModule()
docs()
returnsList()
}) {
// do
val html = handleRequest(HttpMethod.Get, "/docs").response.content
// expected
val expected = TestData.getFileSnapshot("redoc.html")
assertEquals(expected, html)
}
}
private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
@ -440,12 +457,7 @@ internal class KompendiumTest {
}
}
private fun Application.openApiModule() {
routing {
route("/openapi.json") {
get {
call.respond(
Kompendium.openApiSpec.copy(
private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo(
title = "Test API",
version = "1.33.7",
@ -472,9 +484,11 @@ internal class KompendiumTest {
)
)
)
)
}
}
private fun Application.docs() {
routing {
openApi(oas)
redoc(oas)
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Test API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>body { margin: 0; padding: 0; }</style>
</head>
<body><redoc spec-url='/openapi.json'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
</body>
</html>

View File

@ -6,30 +6,18 @@ import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.html.respondHtml
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import java.net.URI
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.link
import kotlinx.html.meta
import kotlinx.html.script
import kotlinx.html.style
import kotlinx.html.title
import kotlinx.html.unsafe
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Kompendium.notarizedDelete
import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.Kompendium.notarizedPost
import org.leafygreens.kompendium.Kompendium.notarizedPut
import org.leafygreens.kompendium.Kompendium.openApiSpec
import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam
@ -45,8 +33,38 @@ import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
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.util.KompendiumHttpCodes
private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
termsOfService = URI("https://example.com"),
contact = OpenApiSpecInfoContact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = OpenApiSpecInfoLicense(
name = "MIT",
url = URI("https://github.com/lg-backbone/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
OpenApiSpecServer(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
OpenApiSpecServer(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
fun main() {
embeddedServer(
Netty,
@ -63,8 +81,8 @@ fun Application.mainModule() {
}
}
routing {
openApi()
redoc()
openApi(oas)
redoc(oas)
route("/test") {
route("/{id}") {
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
@ -163,76 +181,3 @@ object KompendiumTOC {
)
)
}
fun Routing.openApi() {
route("/openapi.json") {
get {
call.respond(
openApiSpec.copy(
info = OpenApiSpecInfo(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
termsOfService = URI("https://example.com"),
contact = OpenApiSpecInfoContact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = OpenApiSpecInfoLicense(
name = "MIT",
url = URI("https://github.com/lg-backbone/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
OpenApiSpecServer(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
OpenApiSpecServer(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
)
}
}
}
fun Routing.redoc() {
route("/docs") {
get {
call.respondHtml {
head {
title {
+"${openApiSpec.info.title}"
}
meta {
charset = "utf-8"
}
meta {
name = "viewport"
content = "width=device-width, initial-scale=1"
}
link {
href = "https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel = "stylesheet"
}
style {
unsafe {
raw("body { margin: 0; padding: 0; }")
}
}
}
body {
// TODO needs to mirror openApi route
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
script {
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
}
}
}
}
}
}