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 # 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 ## [0.4.0] - April 17th, 2021
### Added ### Added

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=0.4.0 project.version=0.5.0
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # 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.jackson.jackson
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.route import io.ktor.routing.route
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest 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.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer 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.ComplexRequest
import org.leafygreens.kompendium.util.KompendiumHttpCodes import org.leafygreens.kompendium.util.KompendiumHttpCodes
import org.leafygreens.kompendium.util.TestCreatedResponse import org.leafygreens.kompendium.util.TestCreatedResponse
import org.leafygreens.kompendium.util.TestData import org.leafygreens.kompendium.util.TestData
import org.leafygreens.kompendium.util.TestDeleteResponse
import org.leafygreens.kompendium.util.TestParams import org.leafygreens.kompendium.util.TestParams
import org.leafygreens.kompendium.util.TestRequest import org.leafygreens.kompendium.util.TestRequest
import org.leafygreens.kompendium.util.TestResponse import org.leafygreens.kompendium.util.TestResponse
@ -56,7 +56,7 @@ internal class KompendiumTest {
fun `Notarized Get records all expected information`() { fun `Notarized Get records all expected information`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedGetModule() notarizedGetModule()
}) { }) {
// do // do
@ -72,7 +72,7 @@ internal class KompendiumTest {
fun `Notarized Get does not interrupt the pipeline`() { fun `Notarized Get does not interrupt the pipeline`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedGetModule() notarizedGetModule()
}) { }) {
// do // do
@ -88,7 +88,7 @@ internal class KompendiumTest {
fun `Notarized Post records all expected information`() { fun `Notarized Post records all expected information`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedPostModule() notarizedPostModule()
}) { }) {
// do // do
@ -104,7 +104,7 @@ internal class KompendiumTest {
fun `Notarized post does not interrupt the pipeline`() { fun `Notarized post does not interrupt the pipeline`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedPostModule() notarizedPostModule()
}) { }) {
// do // do
@ -120,7 +120,7 @@ internal class KompendiumTest {
fun `Notarized Put records all expected information`() { fun `Notarized Put records all expected information`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedPutModule() notarizedPutModule()
}) { }) {
// do // do
@ -137,7 +137,7 @@ internal class KompendiumTest {
fun `Notarized put does not interrupt the pipeline`() { fun `Notarized put does not interrupt the pipeline`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedPutModule() notarizedPutModule()
}) { }) {
// do // do
@ -153,7 +153,7 @@ internal class KompendiumTest {
fun `Notarized delete records all expected information`() { fun `Notarized delete records all expected information`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedDeleteModule() notarizedDeleteModule()
}) { }) {
// do // do
@ -169,7 +169,7 @@ internal class KompendiumTest {
fun `Notarized delete does not interrupt the pipeline`() { fun `Notarized delete does not interrupt the pipeline`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
notarizedDeleteModule() notarizedDeleteModule()
}) { }) {
// do // do
@ -184,7 +184,7 @@ internal class KompendiumTest {
fun `Path parser stores the expected path`() { fun `Path parser stores the expected path`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
pathParsingTestModule() pathParsingTestModule()
}) { }) {
// do // do
@ -200,7 +200,7 @@ internal class KompendiumTest {
fun `Can notarize the root route`() { fun `Can notarize the root route`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
rootModule() rootModule()
}) { }) {
// do // do
@ -216,7 +216,7 @@ internal class KompendiumTest {
fun `Can call the root route`() { fun `Can call the root route`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
rootModule() rootModule()
}) { }) {
// do // do
@ -232,7 +232,7 @@ internal class KompendiumTest {
fun `Can notarize a trailing slash route`() { fun `Can notarize a trailing slash route`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
trailingSlash() trailingSlash()
}) { }) {
// do // do
@ -248,7 +248,7 @@ internal class KompendiumTest {
fun `Can call a trailing slash route`() { fun `Can call a trailing slash route`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
trailingSlash() trailingSlash()
}) { }) {
// do // do
@ -264,7 +264,7 @@ internal class KompendiumTest {
fun `Can notarize a complex type`() { fun `Can notarize a complex type`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
complexType() complexType()
}) { }) {
// do // do
@ -280,7 +280,7 @@ internal class KompendiumTest {
fun `Can notarize primitives`() { fun `Can notarize primitives`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
primitives() primitives()
}) { }) {
// do // do
@ -296,7 +296,7 @@ internal class KompendiumTest {
fun `Can notarize a top level list response`() { fun `Can notarize a top level list response`() {
withTestApplication({ withTestApplication({
configModule() configModule()
openApiModule() docs()
returnsList() returnsList()
}) { }) {
// do // 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 { private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor") val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor") val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
@ -440,41 +457,38 @@ internal class KompendiumTest {
} }
} }
private fun Application.openApiModule() { 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"
)
)
)
private fun Application.docs() {
routing { routing {
route("/openapi.json") { openApi(oas)
get { redoc(oas)
call.respond(
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"
)
)
)
)
}
}
} }
} }

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.call
import io.ktor.application.install import io.ktor.application.install
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
import io.ktor.html.respondHtml
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.Routing
import io.ktor.routing.get
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 java.net.URI import java.net.URI
import kotlinx.html.body import org.leafygreens.kompendium.Kompendium
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.notarizedDelete import org.leafygreens.kompendium.Kompendium.notarizedDelete
import org.leafygreens.kompendium.Kompendium.notarizedGet import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.Kompendium.notarizedPost import org.leafygreens.kompendium.Kompendium.notarizedPost
import org.leafygreens.kompendium.Kompendium.notarizedPut import org.leafygreens.kompendium.Kompendium.notarizedPut
import org.leafygreens.kompendium.Kompendium.openApiSpec
import org.leafygreens.kompendium.annotations.KompendiumField import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.PathParam import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam 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.testSingleGetInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePostInfo 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.redoc
import org.leafygreens.kompendium.util.KompendiumHttpCodes 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() { fun main() {
embeddedServer( embeddedServer(
Netty, Netty,
@ -63,8 +81,8 @@ fun Application.mainModule() {
} }
} }
routing { routing {
openApi() openApi(oas)
redoc() redoc(oas)
route("/test") { route("/test") {
route("/{id}") { route("/{id}") {
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) { 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"
}
}
}
}
}
}