feat: schema configurator to enable field name overrides and transient field omission (#302)
This commit is contained in:
@ -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"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user