feat: allow for overriding openapi endpoint (#192)
This commit is contained in:
@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Ability to override serializer via custom route
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user