diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0391c3b..49d3185bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/gradle.properties b/gradle.properties index 1c879250a..4ed214c41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=0.4.0 +project.version=0.5.0 # Kotlin kotlin.code.style=official # Gradle diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/OpenApiRoute.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/OpenApiRoute.kt new file mode 100644 index 000000000..bb91eb674 --- /dev/null +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/OpenApiRoute.kt @@ -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) + } + } +} diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/Redoc.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/Redoc.kt new file mode 100644 index 000000000..c7bd09fbf --- /dev/null +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/routes/Redoc.kt @@ -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 { +"" } + script { + src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" + } + } + } + } + } +} 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 9a06a7b50..b565fd108 100644 --- a/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/org/leafygreens/kompendium/KompendiumTest.kt @@ -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,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 { - route("/openapi.json") { - get { - 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" - ) - ) - ) - ) - } - } + openApi(oas) + redoc(oas) } } diff --git a/kompendium-core/src/test/resources/redoc.html b/kompendium-core/src/test/resources/redoc.html new file mode 100644 index 000000000..21d16e55a --- /dev/null +++ b/kompendium-core/src/test/resources/redoc.html @@ -0,0 +1,13 @@ + + + + Test API + + + + + + + + + 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 18d53ebd3..78542e196 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 @@ -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(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 { +"" } - script { - src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" - } - } - } - } - } -}