diff --git a/CHANGELOG.md b/CHANGELOG.md index f04ef99e6..763055594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- Ability to override serializer via custom route ### Changed diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt index 7c7a527c0..c2b5fee94 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt @@ -4,12 +4,14 @@ import io.bkbn.kompendium.core.metadata.SchemaMap import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.schema.TypedSchema import io.ktor.application.Application -import io.ktor.application.ApplicationCallPipeline import io.ktor.application.ApplicationFeature import io.ktor.application.call import io.ktor.http.HttpStatusCode -import io.ktor.request.path 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 kotlin.reflect.KClass @@ -19,7 +21,13 @@ class Kompendium(val config: Configuration) { lateinit var spec: OpenApiSpec 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!! fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) { @@ -31,13 +39,8 @@ class Kompendium(val config: Configuration) { override val key: AttributeKey = AttributeKey("Kompendium") override fun install(pipeline: Application, configure: Configuration.() -> Unit): Kompendium { val configuration = Configuration().apply(configure) - - pipeline.intercept(ApplicationCallPipeline.Call) { - if (call.request.path() == configuration.specRoute) { - call.respond(HttpStatusCode.OK, configuration.spec) - } - } - + val routing = pipeline.routing { } + configuration.openApiJson(routing, configuration.spec) return Kompendium(configuration) } } diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 9524889f1..6aefbfb07 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -1,8 +1,13 @@ 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.compareOpenAPISpec import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot 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.constrainedDoubleInfo 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.withExamples import io.bkbn.kompendium.core.util.withOperationId +import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule 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.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({ describe("Notarized Open API Metadata Tests") { @@ -258,4 +276,59 @@ class KompendiumTest : DescribeSpec({ 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") + } + } + } }) diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt index 3e1818cb9..0b5163f29 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt @@ -35,7 +35,7 @@ object TestHelpers { * 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 */ - private fun TestApplicationEngine.compareOpenAPISpec(snapshotName: String) { + fun TestApplicationEngine.compareOpenAPISpec(snapshotName: String) { // act handleRequest(HttpMethod.Get, OPEN_API_ENDPOINT).apply { // assert diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModules.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModules.kt index 191820b36..ec9ec6e49 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModules.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModules.kt @@ -3,19 +3,14 @@ package io.bkbn.kompendium.core.fixtures import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.SerializationFeature 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.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.install import io.ktor.features.ContentNegotiation import io.ktor.http.ContentType import io.ktor.jackson.jackson import io.ktor.routing.routing -import java.net.URI fun Application.docs() { routing { @@ -34,32 +29,6 @@ fun Application.jacksonConfigModule() { fun Application.kompendium() { install(Kompendium) { - spec = 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" - ) - ) - ) + spec = defaultSpec() } } diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestSpecs.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestSpecs.kt new file mode 100644 index 000000000..b842f2095 --- /dev/null +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestSpecs.kt @@ -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" + ) + ) + ) + } +} diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SerializerOverridePlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SerializerOverridePlayground.kt new file mode 100644 index 000000000..8aeaad1f5 --- /dev/null +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SerializerOverridePlayground.kt @@ -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( + 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 + } +}