feat: allow for overriding openapi endpoint (#192)

This commit is contained in:
Ryan Brink
2022-02-11 08:04:47 -05:00
committed by GitHub
parent 2cebb66134
commit 69d6b1af1d
7 changed files with 215 additions and 44 deletions

View File

@ -3,6 +3,7 @@
## Unreleased ## Unreleased
### Added ### Added
- Ability to override serializer via custom route
### Changed ### Changed

View File

@ -4,12 +4,14 @@ import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.schema.TypedSchema import io.bkbn.kompendium.oas.schema.TypedSchema
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature import io.ktor.application.ApplicationFeature
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.request.path
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.util.AttributeKey import io.ktor.util.AttributeKey
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -19,7 +21,13 @@ class Kompendium(val config: Configuration) {
lateinit var spec: OpenApiSpec lateinit var spec: OpenApiSpec
var cache: SchemaMap = mutableMapOf() var cache: SchemaMap = mutableMapOf()
var specRoute = "/openapi.json" var openApiJson: Routing.(OpenApiSpec) -> Unit = { spec ->
route("/openapi.json") {
get {
call.respond(HttpStatusCode.OK, spec)
}
}
}
// TODO Add tests for this!! // TODO Add tests for this!!
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) { fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
@ -31,13 +39,8 @@ class Kompendium(val config: Configuration) {
override val key: AttributeKey<Kompendium> = AttributeKey("Kompendium") override val key: AttributeKey<Kompendium> = AttributeKey("Kompendium")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Kompendium { override fun install(pipeline: Application, configure: Configuration.() -> Unit): Kompendium {
val configuration = Configuration().apply(configure) val configuration = Configuration().apply(configure)
val routing = pipeline.routing { }
pipeline.intercept(ApplicationCallPipeline.Call) { configuration.openApiJson(routing, configuration.spec)
if (call.request.path() == configuration.specRoute) {
call.respond(HttpStatusCode.OK, configuration.spec)
}
}
return Kompendium(configuration) return Kompendium(configuration)
} }
} }

View File

@ -1,8 +1,13 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import io.bkbn.kompendium.core.fixtures.TestHelpers.apiFunctionalityTest import io.bkbn.kompendium.core.fixtures.TestHelpers.apiFunctionalityTest
import io.bkbn.kompendium.core.fixtures.TestHelpers.compareOpenAPISpec
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.fixtures.docs
import io.bkbn.kompendium.core.util.complexType import io.bkbn.kompendium.core.util.complexType
import io.bkbn.kompendium.core.util.constrainedDoubleInfo import io.bkbn.kompendium.core.util.constrainedDoubleInfo
import io.bkbn.kompendium.core.util.constrainedIntInfo import io.bkbn.kompendium.core.util.constrainedIntInfo
@ -53,9 +58,22 @@ import io.bkbn.kompendium.core.util.uniqueArray
import io.bkbn.kompendium.core.util.withDefaultParameter import io.bkbn.kompendium.core.util.withDefaultParameter
import io.bkbn.kompendium.core.util.withExamples import io.bkbn.kompendium.core.util.withExamples
import io.bkbn.kompendium.core.util.withOperationId import io.bkbn.kompendium.core.util.withOperationId
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.kotest.core.spec.style.DescribeSpec import io.kotest.core.spec.style.DescribeSpec
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.serialization.json
import io.ktor.server.testing.withTestApplication
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class KompendiumTest : DescribeSpec({ class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") { describe("Notarized Open API Metadata Tests") {
@ -258,4 +276,59 @@ class KompendiumTest : DescribeSpec({
openApiTestAllSerializers("free_form_object.json") { freeFormObject() } openApiTestAllSerializers("free_form_object.json") { freeFormObject() }
} }
} }
describe("Serialization overrides") {
it("Can override the jackson serializer") {
withTestApplication({
install(Kompendium) {
spec = defaultSpec()
openApiJson = { spec ->
val om = ObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
route("/openapi.json") {
get {
call.respondText { om.writeValueAsString(spec) }
}
}
}
}
install(ContentNegotiation) {
jackson(ContentType.Application.Json)
}
docs()
withExamples()
}) {
compareOpenAPISpec("example_req_and_resp.json")
}
}
it("Can override the kotlinx serializer") {
withTestApplication({
install(Kompendium) {
spec = defaultSpec()
openApiJson = { spec ->
val om = ObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
route("/openapi.json") {
get {
val customSerializer = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
call.respondText { customSerializer.encodeToString(spec) }
}
}
}
}
install(ContentNegotiation) {
json()
}
docs()
withExamples()
}) {
compareOpenAPISpec("example_req_and_resp.json")
}
}
}
}) })

View File

@ -35,7 +35,7 @@ object TestHelpers {
* exists as expected, and that the content matches the expected blob found in the specified file * exists as expected, and that the content matches the expected blob found in the specified file
* @param snapshotName The snapshot file to retrieve from the resources folder * @param snapshotName The snapshot file to retrieve from the resources folder
*/ */
private fun TestApplicationEngine.compareOpenAPISpec(snapshotName: String) { fun TestApplicationEngine.compareOpenAPISpec(snapshotName: String) {
// act // act
handleRequest(HttpMethod.Get, OPEN_API_ENDPOINT).apply { handleRequest(HttpMethod.Get, OPEN_API_ENDPOINT).apply {
// assert // assert

View File

@ -3,19 +3,14 @@ package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.Kompendium import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.install import io.ktor.application.install
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.routing.routing import io.ktor.routing.routing
import java.net.URI
fun Application.docs() { fun Application.docs() {
routing { routing {
@ -34,32 +29,6 @@ fun Application.jacksonConfigModule() {
fun Application.kompendium() { fun Application.kompendium() {
install(Kompendium) { install(Kompendium) {
spec = OpenApiSpec( spec = defaultSpec()
info = Info(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
} }
} }

View File

@ -0,0 +1,40 @@
package io.bkbn.kompendium.core.fixtures
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import java.net.URI
object TestSpecs {
val defaultSpec: () -> OpenApiSpec = {
OpenApiSpec(
info = Info(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
}

View File

@ -0,0 +1,85 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.Customization.customSerializer
import io.bkbn.kompendium.playground.SerializerOverridePlaygroundToC.getExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
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.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
json(Json {
encodeDefaults = false
})
}
install(Kompendium) {
spec = Util.baseSpec
openApiJson = { spec ->
route("/openapi.json") {
get {
call.respondText { customSerializer.encodeToString(spec) }
}
}
}
}
routing {
swagger(pageTitle = "Docs")
notarizedGet(getExample) {
call.respond(HttpStatusCode.OK, SerializerOverrideModel.OhYeaCoolData(null))
}
}
}
object SerializerOverridePlaygroundToC {
val getExample = GetInfo<Unit, SerializerOverrideModel.OhYeaCoolData>(
summary = "Overriding the serializer",
description = "Pretty neat!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
examples = mapOf("demo" to SerializerOverrideModel.OhYeaCoolData(null))
),
tags = setOf("Custom")
)
}
object SerializerOverrideModel {
@Serializable
data class OhYeaCoolData(val num: Int?, val test: String = "gonezo")
}
object Customization {
val customSerializer = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
}