feat: schema configurator to enable field name overrides and transient field omission (#302)

This commit is contained in:
Stuart Campbell
2022-08-23 13:02:50 +01:00
committed by GitHub
parent bf31dd213c
commit 07bdeda364
28 changed files with 581 additions and 85 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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!!!

View File

@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"nonTransient": {
"type": "string"
}
},
"required": [
"nonTransient"
]
}

View File

@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"backed": {
"type": "string"
}
},
"required": [
"backed"
]
}

View File

@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"snake_case_name": {
"type": "string"
}
},
"required": [
"snake_case_name"
]
}