feat: schema configurator to enable field name overrides and transient field omission (#302)
This commit is contained in:
@ -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
|
||||
|
@ -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>("OpenApiSpec")
|
||||
val schemaConfigurator = AttributeKey<SchemaConfigurator>("SchemaConfigurator")
|
||||
}
|
||||
|
@ -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<KType, JsonSchema> = 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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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<FlibbityGibbit>()
|
||||
|
||||
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
||||
|
||||
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbakcedObject>()
|
||||
|
||||
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
||||
|
||||
fun Routing.polymorphicCollectionResponse() = basicGetGenerator<List<FlibbityGibbit>>()
|
||||
|
||||
fun Routing.polymorphicMapResponse() = basicGetGenerator<Map<String, FlibbityGibbit>>()
|
||||
|
72
core/src/test/resources/T0048__ignored_property.json
Normal file
72
core/src/test/resources/T0048__ignored_property.json
Normal file
@ -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": []
|
||||
}
|
72
core/src/test/resources/T0049__unbacked_property.json
Normal file
72
core/src/test/resources/T0049__unbacked_property.json
Normal file
@ -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": []
|
||||
}
|
72
core/src/test/resources/T0050__custom_named_property.json
Normal file
72
core/src/test/resources/T0050__custom_named_property.json
Normal file
@ -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": []
|
||||
}
|
@ -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<KProperty1<out Any, *>> {
|
||||
// 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<Expose>() }
|
||||
return if(hasAnyExpose) {
|
||||
clazz.memberProperties
|
||||
.filter { it.hasJavaAnnotation<Expose>() }
|
||||
} else clazz.memberProperties
|
||||
}
|
||||
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String =
|
||||
property.getJavaAnnotation<SerializedName>()?.value?: property.name
|
||||
|
||||
}
|
||||
|
||||
class JacksonSchemaConfigurator: SchemaConfigurator {
|
||||
|
||||
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>> =
|
||||
clazz.memberProperties
|
||||
.filterNot {
|
||||
it.hasJavaAnnotation<JsonIgnore>()
|
||||
}
|
||||
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String =
|
||||
property.getJavaAnnotation<JsonProperty>()?.value?: property.name
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
|
||||
return javaField?.isAnnotationPresent(T::class.java)?: false
|
||||
}
|
||||
|
||||
inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
|
||||
return javaField?.getDeclaredAnnotation(T::class.java)
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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<KProperty1<out Any, *>> =
|
||||
clazz.memberProperties
|
||||
.filterNot { it.hasAnnotation<Transient>() }
|
||||
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String =
|
||||
property.annotations
|
||||
.filterIsInstance<SerialName>()
|
||||
.firstOrNull()?.value?: property.name
|
||||
}
|
@ -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<KProperty1<out Any, *>>
|
||||
fun serializableName(property: KProperty1<out Any, *>): String
|
||||
|
||||
open class Default: SchemaConfigurator {
|
||||
override fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
|
||||
= clazz.memberProperties
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String
|
||||
= property.name
|
||||
}
|
||||
|
||||
}
|
@ -17,10 +17,17 @@ import kotlin.reflect.typeOf
|
||||
import java.util.UUID
|
||||
|
||||
object SchemaGenerator {
|
||||
inline fun <reified T : Any?> fromTypeToSchema(cache: MutableMap<String, JsonSchema> = mutableMapOf()) =
|
||||
fromTypeToSchema(typeOf<T>(), cache)
|
||||
|
||||
fun fromTypeToSchema(type: KType, cache: MutableMap<String, JsonSchema>): JsonSchema {
|
||||
inline fun <reified T : Any?> fromTypeToSchema(
|
||||
cache: MutableMap<String, JsonSchema> = mutableMapOf(),
|
||||
schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default()
|
||||
) = fromTypeToSchema(typeOf<T>(), cache, schemaConfigurator)
|
||||
|
||||
fun fromTypeToSchema(
|
||||
type: KType,
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
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<String, JsonSchema> = mutableMapOf()): JsonSchema? =
|
||||
fun fromTypeOrUnit(
|
||||
type: KType,
|
||||
cache: MutableMap<String, JsonSchema> = 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
|
||||
}
|
||||
|
@ -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<String, JsonSchema>): JsonSchema {
|
||||
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, 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())
|
||||
|
@ -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<String, JsonSchema>): JsonSchema {
|
||||
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, 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())
|
||||
|
@ -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<String, JsonSchema>): JsonSchema {
|
||||
fun handle(
|
||||
type: KType,
|
||||
clazz: KClass<*>,
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
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())
|
||||
|
@ -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<String, JsonSchema>): JsonSchema {
|
||||
fun handle(
|
||||
type: KType,
|
||||
clazz: KClass<*>,
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
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<KTypeParameter, KTypeProjection>,
|
||||
prop: KProperty<*>,
|
||||
cache: MutableMap<String, JsonSchema>
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
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<KTypeParameter, KTypeProjection>,
|
||||
cache: MutableMap<String, JsonSchema>
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
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<String, JsonSchema>): JsonSchema =
|
||||
SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let {
|
||||
private fun handleProperty(
|
||||
prop: KProperty<*>,
|
||||
cache: MutableMap<String, JsonSchema>,
|
||||
schemaConfigurator: SchemaConfigurator
|
||||
): JsonSchema =
|
||||
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
|
||||
if (it.isOrContainsObjectDef()) {
|
||||
cache[prop.returnType.getSimpleSlug()] = it
|
||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
||||
|
@ -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<SlammaJamma>("T0016__recursive_object.json")
|
||||
}
|
||||
it("Can generate the schema for object with transient property") {
|
||||
jsonSchemaTest<TransientObject>("T0018__transient_object.json")
|
||||
}
|
||||
it("Can generate the schema for object with unbacked property") {
|
||||
jsonSchemaTest<UnbakcedObject>("T0019__unbacked_object.json")
|
||||
}
|
||||
it("Can generate the schema for object with SerialName annotation") {
|
||||
jsonSchemaTest<SerialNameObject>("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 <reified T> jsonSchemaTest(snapshotName: String) {
|
||||
// act
|
||||
val schema = SchemaGenerator.fromTypeToSchema<T>()
|
||||
val schema = SchemaGenerator.fromTypeToSchema<T>(schemaConfigurator = KotlinXSchemaConfigurator())
|
||||
|
||||
// todo add cache assertions!!!
|
||||
|
||||
|
11
json-schema/src/test/resources/T0018__transient_object.json
Normal file
11
json-schema/src/test/resources/T0018__transient_object.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nonTransient": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"nonTransient"
|
||||
]
|
||||
}
|
11
json-schema/src/test/resources/T0019__unbacked_object.json
Normal file
11
json-schema/src/test/resources/T0019__unbacked_object.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backed": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"backed"
|
||||
]
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snake_case_name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"snake_case_name"
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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<KProperty1<out Any, *>> {
|
||||
return if(excludeFieldsWithoutExposeAnnotation) clazz.memberProperties
|
||||
.filter { it.hasJavaAnnotation<Expose>() }
|
||||
else clazz.memberProperties
|
||||
}
|
||||
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String =
|
||||
property.getJavaAnnotation<SerializedName>()?.value?: property.name
|
||||
}
|
||||
|
||||
private inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
|
||||
return javaField?.isAnnotationPresent(T::class.java)?: false
|
||||
}
|
||||
|
||||
private inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
|
||||
return javaField?.getDeclaredAnnotation(T::class.java)
|
||||
}
|
||||
|
@ -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<KProperty1<out Any, *>> =
|
||||
clazz.memberProperties
|
||||
.filterNot {
|
||||
it.hasJavaAnnotation<JsonIgnore>()
|
||||
}
|
||||
|
||||
override fun serializableName(property: KProperty1<out Any, *>): String =
|
||||
property.getJavaAnnotation<JsonProperty>()?.value?: property.name
|
||||
|
||||
}
|
||||
|
||||
private inline fun <reified T: Annotation> KProperty1<*, *>.hasJavaAnnotation(): Boolean {
|
||||
return javaField?.isAnnotationPresent(T::class.java)?: false
|
||||
}
|
||||
|
||||
private inline fun <reified T: Annotation> KProperty1<*, *>.getJavaAnnotation(): T? {
|
||||
return javaField?.getDeclaredAnnotation(T::class.java)
|
||||
}
|
||||
|
Reference in New Issue
Block a user