From 07bdeda3641c509ef89fc28670646833adb2a687 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Tue, 23 Aug 2022 13:02:50 +0100 Subject: [PATCH] feat: schema configurator to enable field name overrides and transient field omission (#302) --- CHANGELOG.md | 4 ++ .../core/attribute/KompendiumAttributes.kt | 2 + .../core/plugin/NotarizedApplication.kt | 4 +- .../kompendium/core/plugin/NotarizedRoute.kt | 15 ++-- .../io/bkbn/kompendium/core/util/Helpers.kt | 16 +++-- .../io/bkbn/kompendium/core/KompendiumTest.kt | 14 ++++ .../bkbn/kompendium/core/util/TestModules.kt | 29 ++------ .../resources/T0048__ignored_property.json | 72 +++++++++++++++++++ .../resources/T0049__unbacked_property.json | 72 +++++++++++++++++++ .../T0050__custom_named_property.json | 72 +++++++++++++++++++ .../core/fixtures/SchemaConfigurators.kt | 54 ++++++++++++++ .../kompendium/core/fixtures/TestHelpers.kt | 11 +-- .../kompendium/core/fixtures/TestModels.kt | 30 ++++++++ .../json/schema/KotlinXSchemaConfigurator.kt | 20 ++++++ .../json/schema/SchemaConfigurator.kt | 18 +++++ .../kompendium/json/schema/SchemaGenerator.kt | 31 +++++--- .../json/schema/handler/CollectionHandler.kt | 5 +- .../json/schema/handler/MapHandler.kt | 5 +- .../schema/handler/SealedObjectHandler.kt | 10 ++- .../schema/handler/SimpleObjectHandler.kt | 48 ++++++++----- .../json/schema/SchemaGeneratorTest.kt | 18 +++-- .../resources/T0018__transient_object.json | 11 +++ .../resources/T0019__unbacked_object.json | 11 +++ .../resources/T0020__serial_name_object.json | 12 ++++ .../locations/NotarizedLocations.kt | 15 ++-- .../kompendium/playground/BasicPlayground.kt | 4 ++ .../playground/GsonSerializationPlayground.kt | 32 +++++++++ .../JacksonSerializationPlayground.kt | 31 ++++++++ 28 files changed, 581 insertions(+), 85 deletions(-) create mode 100644 core/src/test/resources/T0048__ignored_property.json create mode 100644 core/src/test/resources/T0049__unbacked_property.json create mode 100644 core/src/test/resources/T0050__custom_named_property.json create mode 100644 core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt create mode 100644 json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt create mode 100644 json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt create mode 100644 json-schema/src/test/resources/T0018__transient_object.json create mode 100644 json-schema/src/test/resources/T0019__unbacked_object.json create mode 100644 json-schema/src/test/resources/T0020__serial_name_object.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 410ec5fb0..0b7abfa86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Support for @Transient annotation +- Support for @SerialName annotation on fields +- Supports for un-backed fields, by excluding them from the generated schema. + ### Added ### Changed diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/attribute/KompendiumAttributes.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/attribute/KompendiumAttributes.kt index 348c6a6a4..414b1678a 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/attribute/KompendiumAttributes.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/attribute/KompendiumAttributes.kt @@ -1,8 +1,10 @@ package io.bkbn.kompendium.core.attribute +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.oas.OpenApiSpec import io.ktor.util.AttributeKey object KompendiumAttributes { val openApiSpec = AttributeKey("OpenApiSpec") + val schemaConfigurator = AttributeKey("SchemaConfigurator") } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt index a329652e0..472587562 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedApplication.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.core.plugin import io.bkbn.kompendium.core.attribute.KompendiumAttributes +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.oas.OpenApiSpec @@ -13,7 +14,6 @@ import io.ktor.server.routing.application import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing -import kotlin.reflect.KClass import kotlin.reflect.KType object NotarizedApplication { @@ -28,6 +28,7 @@ object NotarizedApplication { } } var customTypes: Map = emptyMap() + var schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default() } operator fun invoke() = createApplicationPlugin( @@ -41,5 +42,6 @@ object NotarizedApplication { spec.components.schemas[type.getSimpleSlug()] = schema } application.attributes.put(KompendiumAttributes.openApiSpec, spec) + application.attributes.put(KompendiumAttributes.schemaConfigurator, pluginConfig.schemaConfigurator) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt index 6b9456e67..932b82233 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt @@ -63,17 +63,18 @@ object NotarizedRoute { } val spec = application.attributes[KompendiumAttributes.openApiSpec] + val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] val path = Path() path.parameters = pluginConfig.parameters - pluginConfig.get?.addToSpec(path, spec, pluginConfig) - pluginConfig.delete?.addToSpec(path, spec, pluginConfig) - pluginConfig.head?.addToSpec(path, spec, pluginConfig) - pluginConfig.options?.addToSpec(path, spec, pluginConfig) - pluginConfig.post?.addToSpec(path, spec, pluginConfig) - pluginConfig.put?.addToSpec(path, spec, pluginConfig) - pluginConfig.patch?.addToSpec(path, spec, pluginConfig) + pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader) + pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader) pluginConfig.path = path } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt index f41c1fe31..d77e887ba 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt @@ -11,6 +11,7 @@ import io.bkbn.kompendium.core.metadata.PostInfo import io.bkbn.kompendium.core.metadata.PutInfo import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.json.schema.SchemaGenerator +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug @@ -25,20 +26,27 @@ import kotlin.reflect.KType object Helpers { - fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig) { - SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema -> + fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig, schemaConfigurator: SchemaConfigurator) { + SchemaGenerator.fromTypeOrUnit( + this.response.responseType, + spec.components.schemas, schemaConfigurator + )?.let { schema -> spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema } errors.forEach { error -> - SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema -> + SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas, schemaConfigurator)?.let { schema -> spec.components.schemas[error.responseType.getSimpleSlug()] = schema } } when (this) { is MethodInfoWithRequest -> { - SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema -> + SchemaGenerator.fromTypeOrUnit( + this.request.requestType, + spec.components.schemas, + schemaConfigurator + )?.let { schema -> spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema } } diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 86d9eba15..88d1d2ac7 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -4,6 +4,7 @@ import dev.forst.ktor.apikey.apiKey import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.core.util.TestModules.complexRequest import io.bkbn.kompendium.core.util.TestModules.customAuthConfig +import io.bkbn.kompendium.core.util.TestModules.customFieldNameResponse import io.bkbn.kompendium.core.util.TestModules.dateTimeString import io.bkbn.kompendium.core.util.TestModules.defaultAuthConfig import io.bkbn.kompendium.core.util.TestModules.defaultField @@ -19,6 +20,7 @@ import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.TestModules.gnarlyGenericResponse import io.bkbn.kompendium.core.util.TestModules.headerParameter +import io.bkbn.kompendium.core.util.TestModules.ignoredFieldsResponse import io.bkbn.kompendium.core.util.TestModules.multipleAuthStrategies import io.bkbn.kompendium.core.util.TestModules.multipleExceptions import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection @@ -48,6 +50,7 @@ import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse import io.bkbn.kompendium.core.util.TestModules.simplePathParsing import io.bkbn.kompendium.core.util.TestModules.simpleRecursive import io.bkbn.kompendium.core.util.TestModules.trailingSlash +import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.oas.component.Components @@ -192,6 +195,17 @@ class KompendiumTest : DescribeSpec({ openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() } } } + describe("Custom Serializable Reader tests") { + it("Can support ignoring fields") { + openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() } + } + it("Can support un-backed fields") { + openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() } + } + it("Can support custom named fields") { + openApiTestAllSerializers("T0050__custom_named_property.json") { customFieldNameResponse() } + } + } describe("Miscellaneous") { xit("Can generate the necessary ReDoc home page") { // TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt index ad88a99dc..36f839af0 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt @@ -1,27 +1,6 @@ package io.bkbn.kompendium.core.util -import io.bkbn.kompendium.core.fixtures.Barzo -import io.bkbn.kompendium.core.fixtures.ColumnSchema -import io.bkbn.kompendium.core.fixtures.ComplexRequest -import io.bkbn.kompendium.core.fixtures.DateTimeString -import io.bkbn.kompendium.core.fixtures.DefaultField -import io.bkbn.kompendium.core.fixtures.ExceptionResponse -import io.bkbn.kompendium.core.fixtures.Flibbity -import io.bkbn.kompendium.core.fixtures.FlibbityGibbit -import io.bkbn.kompendium.core.fixtures.Foosy -import io.bkbn.kompendium.core.fixtures.Gibbity -import io.bkbn.kompendium.core.fixtures.ManyThings -import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics -import io.bkbn.kompendium.core.fixtures.Nested -import io.bkbn.kompendium.core.fixtures.NullableEnum -import io.bkbn.kompendium.core.fixtures.NullableField -import io.bkbn.kompendium.core.fixtures.Page -import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest -import io.bkbn.kompendium.core.fixtures.TestCreatedResponse -import io.bkbn.kompendium.core.fixtures.TestNested -import io.bkbn.kompendium.core.fixtures.TestRequest -import io.bkbn.kompendium.core.fixtures.TestResponse -import io.bkbn.kompendium.core.fixtures.TestSimpleRequest +import io.bkbn.kompendium.core.fixtures.* import io.bkbn.kompendium.core.metadata.DeleteInfo import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.HeadInfo @@ -563,6 +542,12 @@ object TestModules { fun Routing.polymorphicResponse() = basicGetGenerator() + fun Routing.ignoredFieldsResponse() = basicGetGenerator() + + fun Routing.unbackedFieldsResponse() = basicGetGenerator() + + fun Routing.customFieldNameResponse() = basicGetGenerator() + fun Routing.polymorphicCollectionResponse() = basicGetGenerator>() fun Routing.polymorphicMapResponse() = basicGetGenerator>() diff --git a/core/src/test/resources/T0048__ignored_property.json b/core/src/test/resources/T0048__ignored_property.json new file mode 100644 index 000000000..e58f1f7f3 --- /dev/null +++ b/core/src/test/resources/T0048__ignored_property.json @@ -0,0 +1,72 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "info": { + "title": "Test API", + "version": "1.33.7", + "description": "An amazing, fully-ish 😉 generated API spec", + "termsOfService": "https://example.com", + "contact": { + "name": "Homer Simpson", + "url": "https://gph.is/1NPUDiM", + "email": "chunkylover53@aol.com" + }, + "license": { + "name": "MIT", + "url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE" + } + }, + "servers": [ + { + "url": "https://myawesomeapi.com", + "description": "Production instance of my API" + }, + { + "url": "https://staging.myawesomeapi.com", + "description": "Where the fun stuff happens" + } + ], + "paths": { + "/": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransientObject" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "TransientObject": { + "type": "object", + "properties": { + "nonTransient": { + "type": "string" + } + }, + "required": [ + "nonTransient" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/core/src/test/resources/T0049__unbacked_property.json b/core/src/test/resources/T0049__unbacked_property.json new file mode 100644 index 000000000..1237e4d69 --- /dev/null +++ b/core/src/test/resources/T0049__unbacked_property.json @@ -0,0 +1,72 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "info": { + "title": "Test API", + "version": "1.33.7", + "description": "An amazing, fully-ish 😉 generated API spec", + "termsOfService": "https://example.com", + "contact": { + "name": "Homer Simpson", + "url": "https://gph.is/1NPUDiM", + "email": "chunkylover53@aol.com" + }, + "license": { + "name": "MIT", + "url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE" + } + }, + "servers": [ + { + "url": "https://myawesomeapi.com", + "description": "Production instance of my API" + }, + { + "url": "https://staging.myawesomeapi.com", + "description": "Where the fun stuff happens" + } + ], + "paths": { + "/": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnbakcedObject" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "UnbakcedObject": { + "type": "object", + "properties": { + "backed": { + "type": "string" + } + }, + "required": [ + "backed" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/core/src/test/resources/T0050__custom_named_property.json b/core/src/test/resources/T0050__custom_named_property.json new file mode 100644 index 000000000..1e8abf2d2 --- /dev/null +++ b/core/src/test/resources/T0050__custom_named_property.json @@ -0,0 +1,72 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "info": { + "title": "Test API", + "version": "1.33.7", + "description": "An amazing, fully-ish 😉 generated API spec", + "termsOfService": "https://example.com", + "contact": { + "name": "Homer Simpson", + "url": "https://gph.is/1NPUDiM", + "email": "chunkylover53@aol.com" + }, + "license": { + "name": "MIT", + "url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE" + } + }, + "servers": [ + { + "url": "https://myawesomeapi.com", + "description": "Production instance of my API" + }, + { + "url": "https://staging.myawesomeapi.com", + "description": "Where the fun stuff happens" + } + ], + "paths": { + "/": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SerialNameObject" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "SerialNameObject": { + "type": "object", + "properties": { + "snake_case_name": { + "type": "string" + } + }, + "required": [ + "snake_case_name" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt new file mode 100644 index 000000000..b6135de5a --- /dev/null +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SchemaConfigurators.kt @@ -0,0 +1,54 @@ +package io.bkbn.kompendium.core.fixtures + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import io.bkbn.kompendium.json.schema.SchemaConfigurator +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField + +/* + These are test implementation and may well be a good starting point for creating production ones. + Both Gson and Jackson are complex and can achieve this things is more than one way therefore + these will not always work hence why they are in the test package + */ + +class GsonSchemaConfigurator: SchemaConfigurator { + + override fun serializableMemberProperties(clazz: KClass<*>): Collection> { + // NOTE: This is test logic Expose is set at a global Gson level so configure to match your Gson set up + val hasAnyExpose = clazz.memberProperties.any { it.hasJavaAnnotation() } + return if(hasAnyExpose) { + clazz.memberProperties + .filter { it.hasJavaAnnotation() } + } else clazz.memberProperties + } + + override fun serializableName(property: KProperty1): String = + property.getJavaAnnotation()?.value?: property.name + +} + +class JacksonSchemaConfigurator: SchemaConfigurator { + + override fun serializableMemberProperties(clazz: KClass<*>): Collection> = + clazz.memberProperties + .filterNot { + it.hasJavaAnnotation() + } + + override fun serializableName(property: KProperty1): String = + property.getJavaAnnotation()?.value?: property.name + +} + +inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { + return javaField?.isAnnotationPresent(T::class.java)?: false +} + +inline fun KProperty1<*, *>.getJavaAnnotation(): T? { + return javaField?.getDeclaredAnnotation(T::class.java) +} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt index 5a0abc145..da774ab5f 100644 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt @@ -5,13 +5,10 @@ import com.fasterxml.jackson.databind.SerializationFeature import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.routes.redoc +import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.json.schema.definition.JsonSchema 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.serialization.KompendiumSerializersModule -import io.bkbn.kompendium.oas.server.Server import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.matchers.shouldNot @@ -31,7 +28,6 @@ import io.ktor.server.testing.testApplication import kotlin.reflect.KType import kotlinx.serialization.json.Json import java.io.File -import java.net.URI object TestHelpers { private const val OPEN_API_ENDPOINT = "/openapi.json" @@ -83,6 +79,11 @@ object TestHelpers { install(NotarizedApplication()) { customTypes = typeOverrides spec = defaultSpec().specOverrides() + schemaConfigurator = when (serializer) { + SupportedSerializer.KOTLINX -> KotlinXSchemaConfigurator() + SupportedSerializer.GSON -> GsonSchemaConfigurator() + SupportedSerializer.JACKSON -> JacksonSchemaConfigurator() + } } install(ContentNegotiation) { when (serializer) { diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt index b831f1f2e..c40679f9c 100644 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt @@ -1,6 +1,12 @@ package io.bkbn.kompendium.core.fixtures +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import java.time.Instant @Serializable @@ -146,3 +152,27 @@ object Nested { @Serializable data class Response(val idk: Boolean) } + +@Serializable +data class TransientObject( + @field:Expose + val nonTransient: String, + @field:JsonIgnore + @Transient + val transient: String = "transient" +) + +@Serializable +data class UnbakcedObject( + val backed: String +) { + val unbacked: String get() = "unbacked" +} + +@Serializable +data class SerialNameObject( + @field:JsonProperty("snake_case_name") + @field:SerializedName("snake_case_name") + @SerialName("snake_case_name") + val camelCaseName: String +) diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt new file mode 100644 index 000000000..06153da62 --- /dev/null +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/KotlinXSchemaConfigurator.kt @@ -0,0 +1,20 @@ +package io.bkbn.kompendium.json.schema + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Transient +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.memberProperties + +class KotlinXSchemaConfigurator: SchemaConfigurator { + + override fun serializableMemberProperties(clazz: KClass<*>): Collection> = + clazz.memberProperties + .filterNot { it.hasAnnotation() } + + override fun serializableName(property: KProperty1): String = + property.annotations + .filterIsInstance() + .firstOrNull()?.value?: property.name +} diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt new file mode 100644 index 000000000..41a692ef9 --- /dev/null +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaConfigurator.kt @@ -0,0 +1,18 @@ +package io.bkbn.kompendium.json.schema + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties + +interface SchemaConfigurator { + fun serializableMemberProperties(clazz: KClass<*>): Collection> + fun serializableName(property: KProperty1): String + + open class Default: SchemaConfigurator { + override fun serializableMemberProperties(clazz: KClass<*>): Collection> + = clazz.memberProperties + override fun serializableName(property: KProperty1): String + = property.name + } + +} diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaGenerator.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaGenerator.kt index ef4c91f49..01a9dcd18 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaGenerator.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/SchemaGenerator.kt @@ -17,10 +17,17 @@ import kotlin.reflect.typeOf import java.util.UUID object SchemaGenerator { - inline fun fromTypeToSchema(cache: MutableMap = mutableMapOf()) = - fromTypeToSchema(typeOf(), cache) - fun fromTypeToSchema(type: KType, cache: MutableMap): JsonSchema { + inline fun fromTypeToSchema( + cache: MutableMap = mutableMapOf(), + schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default() + ) = fromTypeToSchema(typeOf(), cache, schemaConfigurator) + + fun fromTypeToSchema( + type: KType, + cache: MutableMap, + schemaConfigurator: SchemaConfigurator + ): JsonSchema { cache[type.getSimpleSlug()]?.let { return it } @@ -41,26 +48,30 @@ object SchemaGenerator { UUID::class -> checkForNull(type, TypeDefinition.UUID) else -> when { clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz) - clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache) - clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache) + clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator) + clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator) else -> { if (clazz.isSealed) { - SealedObjectHandler.handle(type, clazz, cache) + SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator) } else { - SimpleObjectHandler.handle(type, clazz, cache) + SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator) } } } } } - fun fromTypeOrUnit(type: KType, cache: MutableMap = mutableMapOf()): JsonSchema? = + fun fromTypeOrUnit( + type: KType, + cache: MutableMap = mutableMapOf(), + schemaConfigurator: SchemaConfigurator + ): JsonSchema? = when (type.classifier as KClass<*>) { Unit::class -> null - else -> fromTypeToSchema(type, cache) + else -> fromTypeToSchema(type, cache, schemaConfigurator) } - private fun checkForNull(type: KType, schema: JsonSchema): JsonSchema = when (type.isMarkedNullable) { + private fun checkForNull(type: KType, schema: JsonSchema, ): JsonSchema = when (type.isMarkedNullable) { true -> OneOfDefinition(NullableDefinition(), schema) false -> schema } diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/CollectionHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/CollectionHandler.kt index 2d2a3d365..3d11c9781 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/CollectionHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/CollectionHandler.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.json.schema.handler import io.bkbn.kompendium.json.schema.SchemaGenerator +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.ArrayDefinition import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.NullableDefinition @@ -13,10 +14,10 @@ import kotlin.reflect.KType object CollectionHandler { - fun handle(type: KType, cache: MutableMap): JsonSchema { + fun handle(type: KType, cache: MutableMap, schemaConfigurator: SchemaConfigurator): JsonSchema { val collectionType = type.arguments.first().type ?: error("This indicates a bug in Kompendium, please open a GitHub issue!") - val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache).let { + val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator).let { if (it is TypeDefinition && it.type == "object") { cache[collectionType.getSimpleSlug()] = it ReferenceDefinition(collectionType.getReferenceSlug()) diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/MapHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/MapHandler.kt index 9d8b3bda7..ddd36d77b 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/MapHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/MapHandler.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.json.schema.handler import io.bkbn.kompendium.json.schema.SchemaGenerator +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.MapDefinition import io.bkbn.kompendium.json.schema.definition.NullableDefinition @@ -14,12 +15,12 @@ import kotlin.reflect.KType object MapHandler { - fun handle(type: KType, cache: MutableMap): JsonSchema { + fun handle(type: KType, cache: MutableMap, schemaConfigurator: SchemaConfigurator): JsonSchema { require(type.arguments.first().type?.classifier as KClass<*> == String::class) { "JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}" } val valueType = type.arguments[1].type ?: error("this indicates a bug in Kompendium, please open a GitHub issue") - val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache).let { + val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator).let { if (it is TypeDefinition && it.type == "object") { cache[valueType.getSimpleSlug()] = it ReferenceDefinition(valueType.getReferenceSlug()) diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt index fc152975c..e63eac476 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SealedObjectHandler.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.json.schema.handler import io.bkbn.kompendium.json.schema.SchemaGenerator +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition @@ -13,11 +14,16 @@ import kotlin.reflect.full.createType object SealedObjectHandler { - fun handle(type: KType, clazz: KClass<*>, cache: MutableMap): JsonSchema { + fun handle( + type: KType, + clazz: KClass<*>, + cache: MutableMap, + schemaConfigurator: SchemaConfigurator + ): JsonSchema { val subclasses = clazz.sealedSubclasses .map { it.createType(type.arguments) } .map { t -> - SchemaGenerator.fromTypeToSchema(t, cache).let { js -> + SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator).let { js -> if (js is TypeDefinition && js.type == "object") { cache[t.getSimpleSlug()] = js ReferenceDefinition(t.getReferenceSlug()) diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt index 5f487c26c..bafcbf2e3 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt @@ -1,6 +1,7 @@ package io.bkbn.kompendium.json.schema.handler import io.bkbn.kompendium.json.schema.SchemaGenerator +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.NullableDefinition import io.bkbn.kompendium.json.schema.definition.OneOfDefinition @@ -14,22 +15,29 @@ import kotlin.reflect.KType import kotlin.reflect.KTypeParameter import kotlin.reflect.KTypeProjection import kotlin.reflect.full.createType -import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.javaField object SimpleObjectHandler { - fun handle(type: KType, clazz: KClass<*>, cache: MutableMap): JsonSchema { + fun handle( + type: KType, + clazz: KClass<*>, + cache: MutableMap, + schemaConfigurator: SchemaConfigurator + ): JsonSchema { cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug()) val typeMap = clazz.typeParameters.zip(type.arguments).toMap() - val props = clazz.memberProperties.associate { prop -> + val props = schemaConfigurator.serializableMemberProperties(clazz) + .filterNot { it.javaField == null } + .associate { prop -> val schema = when (prop.needsToInjectGenerics(typeMap)) { - true -> handleNestedGenerics(typeMap, prop, cache) + true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator) false -> when (typeMap.containsKey(prop.returnType.classifier)) { - true -> handleGenericProperty(prop, typeMap, cache) - false -> handleProperty(prop, cache) + true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator) + false -> handleProperty(prop, cache, schemaConfigurator) } } @@ -38,15 +46,17 @@ object SimpleObjectHandler { false -> schema } - prop.name to nullCheckSchema + schemaConfigurator.serializableName(prop) to nullCheckSchema } - val required = clazz.memberProperties.filterNot { prop -> prop.returnType.isMarkedNullable } + val required = schemaConfigurator.serializableMemberProperties(clazz) + .asSequence() + .filterNot { it.javaField == null } + .filterNot { prop -> prop.returnType.isMarkedNullable } .filterNot { prop -> clazz.primaryConstructor!!.parameters.find { it.name == prop.name }!!.isOptional } - .map { it.name } + .map { schemaConfigurator.serializableName(it) } .toSet() - val definition = TypeDefinition( type = "object", properties = props, @@ -69,7 +79,8 @@ object SimpleObjectHandler { private fun handleNestedGenerics( typeMap: Map, prop: KProperty<*>, - cache: MutableMap + cache: MutableMap, + schemaConfigurator: SchemaConfigurator ): JsonSchema { val propClass = prop.returnType.classifier as KClass<*> val types = prop.returnType.arguments.map { @@ -77,7 +88,7 @@ object SimpleObjectHandler { typeMap.filterKeys { k -> k.name == typeSymbol }.values.first() } val constructedType = propClass.createType(types) - return SchemaGenerator.fromTypeToSchema(constructedType, cache).let { + return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let { if (it.isOrContainsObjectDef()) { cache[constructedType.getSimpleSlug()] = it ReferenceDefinition(prop.returnType.getReferenceSlug()) @@ -90,11 +101,12 @@ object SimpleObjectHandler { private fun handleGenericProperty( prop: KProperty<*>, typeMap: Map, - cache: MutableMap + cache: MutableMap, + schemaConfigurator: SchemaConfigurator ): JsonSchema { val type = typeMap[prop.returnType.classifier]?.type ?: error("This indicates a bug in Kompendium, please open a GitHub issue") - return SchemaGenerator.fromTypeToSchema(type, cache).let { + return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let { if (it.isOrContainsObjectDef()) { cache[type.getSimpleSlug()] = it ReferenceDefinition(type.getReferenceSlug()) @@ -104,8 +116,12 @@ object SimpleObjectHandler { } } - private fun handleProperty(prop: KProperty<*>, cache: MutableMap): JsonSchema = - SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let { + private fun handleProperty( + prop: KProperty<*>, + cache: MutableMap, + schemaConfigurator: SchemaConfigurator + ): JsonSchema = + SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let { if (it.isOrContainsObjectDef()) { cache[prop.returnType.getSimpleSlug()] = it ReferenceDefinition(prop.returnType.getReferenceSlug()) diff --git a/json-schema/src/test/kotlin/io/bkbn/kompendium/json/schema/SchemaGeneratorTest.kt b/json-schema/src/test/kotlin/io/bkbn/kompendium/json/schema/SchemaGeneratorTest.kt index 765212e6c..f308748fd 100644 --- a/json-schema/src/test/kotlin/io/bkbn/kompendium/json/schema/SchemaGeneratorTest.kt +++ b/json-schema/src/test/kotlin/io/bkbn/kompendium/json/schema/SchemaGeneratorTest.kt @@ -1,12 +1,7 @@ package io.bkbn.kompendium.json.schema -import io.bkbn.kompendium.core.fixtures.ComplexRequest -import io.bkbn.kompendium.core.fixtures.FlibbityGibbit -import io.bkbn.kompendium.core.fixtures.SimpleEnum -import io.bkbn.kompendium.core.fixtures.SlammaJamma +import io.bkbn.kompendium.core.fixtures.* import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot -import io.bkbn.kompendium.core.fixtures.TestResponse -import io.bkbn.kompendium.core.fixtures.TestSimpleRequest import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.throwables.shouldThrow @@ -45,6 +40,15 @@ class SchemaGeneratorTest : DescribeSpec({ xit("Can generate the schema for a recursive type") { // TODO jsonSchemaTest("T0016__recursive_object.json") } + it("Can generate the schema for object with transient property") { + jsonSchemaTest("T0018__transient_object.json") + } + it("Can generate the schema for object with unbacked property") { + jsonSchemaTest("T0019__unbacked_object.json") + } + it("Can generate the schema for object with SerialName annotation") { + jsonSchemaTest("T0020__serial_name_object.json") + } } describe("Enums") { it("Can generate the schema for a simple enum") { @@ -91,7 +95,7 @@ class SchemaGeneratorTest : DescribeSpec({ private inline fun jsonSchemaTest(snapshotName: String) { // act - val schema = SchemaGenerator.fromTypeToSchema() + val schema = SchemaGenerator.fromTypeToSchema(schemaConfigurator = KotlinXSchemaConfigurator()) // todo add cache assertions!!! diff --git a/json-schema/src/test/resources/T0018__transient_object.json b/json-schema/src/test/resources/T0018__transient_object.json new file mode 100644 index 000000000..6bdc560ab --- /dev/null +++ b/json-schema/src/test/resources/T0018__transient_object.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "nonTransient": { + "type": "string" + } + }, + "required": [ + "nonTransient" + ] +} diff --git a/json-schema/src/test/resources/T0019__unbacked_object.json b/json-schema/src/test/resources/T0019__unbacked_object.json new file mode 100644 index 000000000..ca8f3dca6 --- /dev/null +++ b/json-schema/src/test/resources/T0019__unbacked_object.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "backed": { + "type": "string" + } + }, + "required": [ + "backed" + ] +} diff --git a/json-schema/src/test/resources/T0020__serial_name_object.json b/json-schema/src/test/resources/T0020__serial_name_object.json new file mode 100644 index 000000000..f6efd49d8 --- /dev/null +++ b/json-schema/src/test/resources/T0020__serial_name_object.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "snake_case_name": { + "type": "string" + } + }, + "required": [ + "snake_case_name" + ] +} + diff --git a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt index 8ee88d020..56cfa4c00 100644 --- a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt +++ b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt @@ -44,16 +44,17 @@ object NotarizedLocations { createConfiguration = ::Config ) { val spec = application.attributes[KompendiumAttributes.openApiSpec] + val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] pluginConfig.locations.forEach { (k, v) -> val path = Path() path.parameters = v.parameters - v.get?.addToSpec(path, spec, v) - v.delete?.addToSpec(path, spec, v) - v.head?.addToSpec(path, spec, v) - v.options?.addToSpec(path, spec, v) - v.post?.addToSpec(path, spec, v) - v.put?.addToSpec(path, spec, v) - v.patch?.addToSpec(path, spec, v) + v.get?.addToSpec(path, spec, v, serializableReader) + v.delete?.addToSpec(path, spec, v, serializableReader) + v.head?.addToSpec(path, spec, v, serializableReader) + v.options?.addToSpec(path, spec, v, serializableReader) + v.post?.addToSpec(path, spec, v, serializableReader) + v.put?.addToSpec(path, spec, v, serializableReader) + v.patch?.addToSpec(path, spec, v, serializableReader) val location = k.getLocationFromClass() spec.paths[location] = path diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt index ae264cd83..ca317baf8 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt @@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.routes.redoc +import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule @@ -42,6 +43,9 @@ private fun Application.mainModule() { } install(NotarizedApplication()) { spec = baseSpec + // Adds support for @Transient and @SerialName + // If you are not using them this is not required. + schemaConfigurator = KotlinXSchemaConfigurator() } routing { redoc(pageTitle = "Simple API Docs") diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt index aa0c879a1..e88ff5d66 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt @@ -1,9 +1,12 @@ package io.bkbn.kompendium.playground +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.routes.redoc +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.playground.util.ExampleResponse @@ -21,6 +24,10 @@ import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField fun main() { embeddedServer( @@ -38,6 +45,7 @@ private fun Application.mainModule() { } install(NotarizedApplication()) { spec = baseSpec + schemaConfigurator = GsonSchemaConfigurator() } routing { redoc(pageTitle = "Simple API Docs") @@ -71,3 +79,27 @@ private fun Route.locationDocumentation() { } } } + +// Adds support for Expose and SerializedName annotations, +// if you are not using them this is not required +class GsonSchemaConfigurator( + private val excludeFieldsWithoutExposeAnnotation: Boolean = false +): SchemaConfigurator.Default() { + + override fun serializableMemberProperties(clazz: KClass<*>): Collection> { + return if(excludeFieldsWithoutExposeAnnotation) clazz.memberProperties + .filter { it.hasJavaAnnotation() } + else clazz.memberProperties + } + + override fun serializableName(property: KProperty1): String = + property.getJavaAnnotation()?.value?: property.name +} + +private inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { + return javaField?.isAnnotationPresent(T::class.java)?: false +} + +private inline fun KProperty1<*, *>.getJavaAnnotation(): T? { + return javaField?.getDeclaredAnnotation(T::class.java) +} diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt index f39605523..b93d3c29f 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt @@ -1,11 +1,14 @@ package io.bkbn.kompendium.playground +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.SerializationFeature import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.routes.redoc +import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.playground.util.ExampleResponse @@ -23,6 +26,10 @@ import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField fun main() { embeddedServer( @@ -41,6 +48,7 @@ private fun Application.mainModule() { } install(NotarizedApplication()) { spec = baseSpec + schemaConfigurator = JacksonSchemaConfigurator() } routing { redoc(pageTitle = "Simple API Docs") @@ -75,3 +83,26 @@ private fun Route.locationDocumentation() { } } +// Adds support for JsonIgnore and JsonProperty annotations, +// if you are not using them this is not required +// This also does not support class level configuration +private class JacksonSchemaConfigurator: SchemaConfigurator.Default() { + + override fun serializableMemberProperties(clazz: KClass<*>): Collection> = + clazz.memberProperties + .filterNot { + it.hasJavaAnnotation() + } + + override fun serializableName(property: KProperty1): String = + property.getJavaAnnotation()?.value?: property.name + +} + +private inline fun KProperty1<*, *>.hasJavaAnnotation(): Boolean { + return javaField?.isAnnotationPresent(T::class.java)?: false +} + +private inline fun KProperty1<*, *>.getJavaAnnotation(): T? { + return javaField?.getDeclaredAnnotation(T::class.java) +}