chore: handler refactor (#179)
This commit is contained in:
@ -5,6 +5,7 @@
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
- Cleaned up and broke out handlers into separate classes
|
||||
|
||||
### Remove
|
||||
|
||||
|
@ -18,12 +18,12 @@ class Kompendium(val config: Configuration) {
|
||||
class Configuration {
|
||||
lateinit var spec: OpenApiSpec
|
||||
|
||||
var cache: SchemaMap = emptyMap()
|
||||
var cache: SchemaMap = mutableMapOf()
|
||||
var specRoute = "/openapi.json"
|
||||
|
||||
// TODO Add tests for this!!
|
||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||
cache = cache.plus(clazz.simpleName!! to schema)
|
||||
cache[clazz.simpleName!!] = schema
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,9 +43,9 @@ object KompendiumPreFlight {
|
||||
}
|
||||
|
||||
fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) {
|
||||
feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
|
||||
feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
|
||||
feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
|
||||
Kontent.generateKontent(requestType, feature.config.cache)
|
||||
Kontent.generateKontent(responseType, feature.config.cache)
|
||||
Kontent.generateKontent(paramType, feature.config.cache)
|
||||
feature.updateReferences()
|
||||
}
|
||||
|
||||
|
@ -1,49 +1,17 @@
|
||||
package io.bkbn.kompendium.core
|
||||
|
||||
import io.bkbn.kompendium.annotations.Field
|
||||
import io.bkbn.kompendium.annotations.FreeFormObject
|
||||
import io.bkbn.kompendium.annotations.Referenced
|
||||
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||
import io.bkbn.kompendium.annotations.constraint.Format
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxItems
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxLength
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxProperties
|
||||
import io.bkbn.kompendium.annotations.constraint.Maximum
|
||||
import io.bkbn.kompendium.annotations.constraint.MinItems
|
||||
import io.bkbn.kompendium.annotations.constraint.MinLength
|
||||
import io.bkbn.kompendium.annotations.constraint.MinProperties
|
||||
import io.bkbn.kompendium.annotations.constraint.Minimum
|
||||
import io.bkbn.kompendium.annotations.constraint.MultipleOf
|
||||
import io.bkbn.kompendium.annotations.constraint.Pattern
|
||||
import io.bkbn.kompendium.annotations.constraint.UniqueItems
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.core.metadata.TypeMap
|
||||
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
|
||||
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
|
||||
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.core.schema.CollectionHandler
|
||||
import io.bkbn.kompendium.core.schema.EnumHandler
|
||||
import io.bkbn.kompendium.core.schema.MapHandler
|
||||
import io.bkbn.kompendium.core.schema.ObjectHandler
|
||||
import io.bkbn.kompendium.core.util.Helpers.logged
|
||||
import io.bkbn.kompendium.core.util.Helpers.toNumber
|
||||
import io.bkbn.kompendium.oas.schema.AnyOfSchema
|
||||
import io.bkbn.kompendium.oas.schema.ArraySchema
|
||||
import io.bkbn.kompendium.oas.schema.ComponentSchema
|
||||
import io.bkbn.kompendium.oas.schema.DictionarySchema
|
||||
import io.bkbn.kompendium.oas.schema.EnumSchema
|
||||
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
||||
import io.bkbn.kompendium.oas.schema.FreeFormSchema
|
||||
import io.bkbn.kompendium.oas.schema.ObjectSchema
|
||||
import io.bkbn.kompendium.oas.schema.ReferencedSchema
|
||||
import io.bkbn.kompendium.oas.schema.SimpleSchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KClassifier
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.typeOf
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigDecimal
|
||||
@ -65,10 +33,10 @@ object Kontent {
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified T> generateKontent(
|
||||
cache: SchemaMap = emptyMap()
|
||||
): SchemaMap {
|
||||
cache: SchemaMap = mutableMapOf()
|
||||
) {
|
||||
val kontentType = typeOf<T>()
|
||||
return generateKTypeKontent(kontentType, cache)
|
||||
generateKTypeKontent(kontentType, cache)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,13 +47,11 @@ object Kontent {
|
||||
*/
|
||||
fun generateKontent(
|
||||
type: KType,
|
||||
cache: SchemaMap = emptyMap()
|
||||
): SchemaMap {
|
||||
var newCache = cache
|
||||
cache: SchemaMap = mutableMapOf()
|
||||
) {
|
||||
gatherSubTypes(type).forEach {
|
||||
newCache = generateKTypeKontent(it, newCache)
|
||||
generateKTypeKontent(it, cache)
|
||||
}
|
||||
return newCache
|
||||
}
|
||||
|
||||
private fun gatherSubTypes(type: KType): List<KType> {
|
||||
@ -106,371 +72,27 @@ object Kontent {
|
||||
*/
|
||||
fun generateKTypeKontent(
|
||||
type: KType,
|
||||
cache: SchemaMap = emptyMap(),
|
||||
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||
cache: SchemaMap = mutableMapOf(),
|
||||
) = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||
logger.debug("Parsing Kontent of $type")
|
||||
when (val clazz = type.classifier as KClass<*>) {
|
||||
Unit::class -> cache
|
||||
Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer"))
|
||||
Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
||||
Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
||||
Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number"))
|
||||
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
||||
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
||||
UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string"))
|
||||
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
||||
BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
||||
ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string"))
|
||||
Int::class -> cache[clazz.simpleName!!] = FormattedSchema("int32", "integer")
|
||||
Long::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
|
||||
Double::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
|
||||
Float::class -> cache[clazz.simpleName!!] = FormattedSchema("float", "number")
|
||||
String::class -> cache[clazz.simpleName!!] = SimpleSchema("string")
|
||||
Boolean::class -> cache[clazz.simpleName!!] = SimpleSchema("boolean")
|
||||
UUID::class -> cache[clazz.simpleName!!] = FormattedSchema("uuid", "string")
|
||||
BigDecimal::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
|
||||
BigInteger::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
|
||||
ByteArray::class -> cache[clazz.simpleName!!] = FormattedSchema("byte", "string")
|
||||
else -> when {
|
||||
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
|
||||
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
|
||||
clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache)
|
||||
else -> handleComplexType(type, clazz, cache)
|
||||
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, clazz, cache)
|
||||
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
|
||||
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, clazz, cache)
|
||||
else -> ObjectHandler.handle(type, clazz, cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
|
||||
* @param clazz Class of the object to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
// This needs to be simple because it will be stored under its appropriate reference component implicitly
|
||||
val slug = type.getSimpleSlug()
|
||||
// Only analyze if component has not already been stored in the cache
|
||||
return when (cache.containsKey(slug)) {
|
||||
true -> {
|
||||
logger.debug("Cache already contains $slug, returning cache untouched")
|
||||
cache
|
||||
}
|
||||
false -> {
|
||||
logger.debug("$slug was not found in cache, generating now")
|
||||
var newCache = cache
|
||||
// If referenced, add tie from simple slug to schema slug
|
||||
if (clazz.hasAnnotation<Referenced>()) {
|
||||
newCache = newCache.plus(type.getSimpleSlug() to ReferencedSchema(type.getReferenceSlug()))
|
||||
}
|
||||
// Grabs any type parameters mapped to the corresponding type argument(s)
|
||||
val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
|
||||
// associates each member with a Pair of prop name to property schema
|
||||
val fieldMap = clazz.memberProperties.associate { prop ->
|
||||
logger.debug("Analyzing $prop in class $clazz")
|
||||
// Grab the field of the current property
|
||||
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
||||
// Short circuit if data is free form
|
||||
val freeForm = prop.findAnnotation<FreeFormObject>()
|
||||
var name = prop.name
|
||||
|
||||
// todo add method to clean up
|
||||
when (freeForm) {
|
||||
null -> {
|
||||
val baseType = scanForGeneric(typeMap, prop)
|
||||
val baseClazz = baseType.classifier as KClass<*>
|
||||
val allTypes = scanForSealed(baseClazz, baseType)
|
||||
newCache = updateCache(newCache, field, allTypes)
|
||||
var propSchema = constructComponentSchema(
|
||||
typeMap = typeMap,
|
||||
prop = prop,
|
||||
fieldClazz = field,
|
||||
clazz = baseClazz,
|
||||
type = baseType,
|
||||
cache = newCache
|
||||
)
|
||||
// todo move to helper
|
||||
prop.findAnnotation<Field>()?.let { fieldOverrides ->
|
||||
if (fieldOverrides.description.isNotBlank()) {
|
||||
propSchema = propSchema.setDescription(fieldOverrides.description)
|
||||
}
|
||||
if (fieldOverrides.name.isNotBlank()) {
|
||||
name = fieldOverrides.name
|
||||
}
|
||||
}
|
||||
Pair(name, propSchema)
|
||||
}
|
||||
else -> {
|
||||
val minProperties = prop.findAnnotation<MinProperties>()
|
||||
val maxProperties = prop.findAnnotation<MaxProperties>()
|
||||
val schema =
|
||||
FreeFormSchema(minProperties = minProperties?.properties, maxProperties = maxProperties?.properties)
|
||||
Pair(name, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Looking for undeclared fields")
|
||||
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
|
||||
val undeclaredType = it.clazz.createType()
|
||||
newCache = generateKontent(undeclaredType, newCache)
|
||||
it.field to newCache[undeclaredType.getSimpleSlug()]!!
|
||||
}
|
||||
logger.debug("$slug contains $fieldMap")
|
||||
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
|
||||
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
|
||||
// todo de-dup this logic
|
||||
if (requiredParams.isNotEmpty()) {
|
||||
schema = schema.copy(required = requiredParams.map { param ->
|
||||
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
|
||||
?.let { field -> field.name.ifBlank { param.name!! } }
|
||||
?: param.name!!
|
||||
})
|
||||
}
|
||||
logger.debug("$slug schema: $schema")
|
||||
newCache.plus(slug to schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the type information provided and adds any missing data to the schema map
|
||||
*/
|
||||
private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>): SchemaMap {
|
||||
var newCache = cache
|
||||
if (!cache.containsKey(clazz.simpleName)) {
|
||||
logger.debug("Cache was missing ${clazz.simpleName}, adding now")
|
||||
types.forEach {
|
||||
newCache = generateKTypeKontent(it, newCache)
|
||||
}
|
||||
}
|
||||
return newCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a class for sealed subclasses. If found, returns a list with all children. Otherwise, returns
|
||||
* the base type
|
||||
*/
|
||||
private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
|
||||
clazz.sealedSubclasses.map { it.createType(type.arguments) }
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Yoinks any generic types from the type map should the field be a generic
|
||||
*/
|
||||
private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
|
||||
if (typeMap.containsKey(prop.returnType.classifier)) {
|
||||
logger.debug("Generic type detected")
|
||||
typeMap[prop.returnType.classifier]?.type!!
|
||||
} else {
|
||||
prop.returnType
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a [ComponentSchema]
|
||||
*/
|
||||
private fun constructComponentSchema(
|
||||
typeMap: TypeMap,
|
||||
clazz: KClass<*>,
|
||||
fieldClazz: KClass<*>,
|
||||
prop: KProperty1<*, *>,
|
||||
type: KType,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema =
|
||||
when (typeMap.containsKey(prop.returnType.classifier)) {
|
||||
true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
|
||||
false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
|
||||
}.scanForConstraints(clazz, prop)
|
||||
|
||||
private fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
|
||||
when (this) {
|
||||
is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
|
||||
is ArraySchema -> scanForConstraints(prop)
|
||||
is DictionarySchema -> this // TODO Anything here?
|
||||
is EnumSchema -> scanForConstraints(prop)
|
||||
is FormattedSchema -> scanForConstraints(prop)
|
||||
is FreeFormSchema -> this // todo anything here?
|
||||
is ObjectSchema -> scanForConstraints(clazz, prop)
|
||||
is SimpleSchema -> scanForConstraints(prop)
|
||||
is ReferencedSchema -> this // todo anything here?
|
||||
}
|
||||
|
||||
private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
|
||||
val minItems = prop.findAnnotation<MinItems>()
|
||||
val maxItems = prop.findAnnotation<MaxItems>()
|
||||
val uniqueItems = prop.findAnnotation<UniqueItems>()
|
||||
|
||||
return this.copy(
|
||||
minItems = minItems?.items,
|
||||
maxItems = maxItems?.items,
|
||||
uniqueItems = uniqueItems?.let { true }
|
||||
)
|
||||
}
|
||||
|
||||
private fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
return this.copy(nullable = true)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
|
||||
val minimum = prop.findAnnotation<Minimum>()
|
||||
val maximum = prop.findAnnotation<Maximum>()
|
||||
val multipleOf = prop.findAnnotation<MultipleOf>()
|
||||
|
||||
var schema = this
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema.copy(
|
||||
minimum = minimum?.min?.toNumber(),
|
||||
maximum = maximum?.max?.toNumber(),
|
||||
exclusiveMinimum = minimum?.exclusive,
|
||||
exclusiveMaximum = maximum?.exclusive,
|
||||
multipleOf = multipleOf?.multiple?.toNumber(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
|
||||
val minLength = prop.findAnnotation<MinLength>()
|
||||
val maxLength = prop.findAnnotation<MaxLength>()
|
||||
val pattern = prop.findAnnotation<Pattern>()
|
||||
val format = prop.findAnnotation<Format>()
|
||||
|
||||
var schema = this
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema.copy(
|
||||
minLength = minLength?.length,
|
||||
maxLength = maxLength?.length,
|
||||
pattern = pattern?.pattern,
|
||||
format = format?.format
|
||||
)
|
||||
}
|
||||
|
||||
private fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
|
||||
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
|
||||
var schema = this
|
||||
|
||||
// todo dedup this
|
||||
if (requiredParams.isNotEmpty()) {
|
||||
schema = schema.copy(required = requiredParams.map { param ->
|
||||
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
|
||||
?.let { field -> field.name.ifBlank { param.name!! } }
|
||||
?: param.name!!
|
||||
})
|
||||
}
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
|
||||
*/
|
||||
private fun handleStandardProperty(
|
||||
clazz: KClass<*>,
|
||||
fieldClazz: KClass<*>,
|
||||
prop: KProperty1<*, *>,
|
||||
type: KType,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema = if (clazz.isSealed) {
|
||||
val refs = clazz.sealedSubclasses
|
||||
.map { it.createType(type.arguments) }
|
||||
.map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
|
||||
AnyOfSchema(refs)
|
||||
} else {
|
||||
val slug = fieldClazz.getSimpleSlug(prop)
|
||||
cache[slug] ?: error("$slug not found in cache")
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
|
||||
*/
|
||||
private fun handleGenericProperty(
|
||||
typeMap: TypeMap,
|
||||
clazz: KClass<*>,
|
||||
type: KType,
|
||||
classifier: KClassifier?,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema = if (clazz.isSealed) {
|
||||
val refs = clazz.sealedSubclasses
|
||||
.map { it.createType(type.arguments) }
|
||||
.map { it.getSimpleSlug() }
|
||||
.map { cache[it] ?: error("$it not available in cache") }
|
||||
AnyOfSchema(refs)
|
||||
} else {
|
||||
val slug = typeMap[classifier]?.type!!.getSimpleSlug()
|
||||
cache[slug] ?: error("$slug not found in cache")
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an [Enum] is encountered
|
||||
* @param clazz Class of the object to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||
return cache.plus(clazz.simpleName!! to EnumSchema(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when a [Map] is encountered
|
||||
* @param type Map type information
|
||||
* @param clazz Map class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
logger.debug("Map detected for $type, generating schema and appending to cache")
|
||||
val (keyType, valType) = type.arguments.map { it.type }
|
||||
logger.debug("Obtained map types -> key: $keyType and value: $valType")
|
||||
if (keyType?.classifier != String::class) {
|
||||
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
|
||||
}
|
||||
var updatedCache = generateKTypeKontent(valType!!, cache)
|
||||
val valClass = valType.classifier as KClass<*>
|
||||
val valClassName = valClass.simpleName
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
val valueReference = when (valClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(valType)
|
||||
AnyOfSchema(subTypes.map {
|
||||
updatedCache = generateKTypeKontent(it, updatedCache)
|
||||
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||
})
|
||||
}
|
||||
false -> updatedCache[valClassName] ?: error("$valClassName not found")
|
||||
}
|
||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||
updatedCache = generateKontent(valType, updatedCache)
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when a [Collection] is encountered
|
||||
* @param type Collection type information
|
||||
* @param clazz Collection class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
logger.debug("Collection detected for $type, generating schema and appending to cache")
|
||||
val collectionType = type.arguments.first().type!!
|
||||
val collectionClass = collectionType.classifier as KClass<*>
|
||||
logger.debug("Obtained collection class: $collectionClass")
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
var updatedCache = generateKTypeKontent(collectionType, cache)
|
||||
val valueReference = when (collectionClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(collectionType)
|
||||
AnyOfSchema(subTypes.map {
|
||||
updatedCache = generateKTypeKontent(it, cache)
|
||||
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||
})
|
||||
}
|
||||
false -> updatedCache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
|
||||
}
|
||||
val schema = ArraySchema(items = valueReference)
|
||||
updatedCache = generateKontent(collectionType, cache)
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,122 @@
|
||||
package io.bkbn.kompendium.core.constraint
|
||||
|
||||
import io.bkbn.kompendium.annotations.Field
|
||||
import io.bkbn.kompendium.annotations.constraint.Format
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxItems
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxLength
|
||||
import io.bkbn.kompendium.annotations.constraint.Maximum
|
||||
import io.bkbn.kompendium.annotations.constraint.MinItems
|
||||
import io.bkbn.kompendium.annotations.constraint.MinLength
|
||||
import io.bkbn.kompendium.annotations.constraint.Minimum
|
||||
import io.bkbn.kompendium.annotations.constraint.MultipleOf
|
||||
import io.bkbn.kompendium.annotations.constraint.Pattern
|
||||
import io.bkbn.kompendium.annotations.constraint.UniqueItems
|
||||
import io.bkbn.kompendium.core.util.Helpers.toNumber
|
||||
import io.bkbn.kompendium.oas.schema.AnyOfSchema
|
||||
import io.bkbn.kompendium.oas.schema.ArraySchema
|
||||
import io.bkbn.kompendium.oas.schema.ComponentSchema
|
||||
import io.bkbn.kompendium.oas.schema.DictionarySchema
|
||||
import io.bkbn.kompendium.oas.schema.EnumSchema
|
||||
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
||||
import io.bkbn.kompendium.oas.schema.FreeFormSchema
|
||||
import io.bkbn.kompendium.oas.schema.ObjectSchema
|
||||
import io.bkbn.kompendium.oas.schema.ReferencedSchema
|
||||
import io.bkbn.kompendium.oas.schema.SimpleSchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
|
||||
when (this) {
|
||||
is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
|
||||
is ArraySchema -> scanForConstraints(prop)
|
||||
is DictionarySchema -> this // TODO Anything here?
|
||||
is EnumSchema -> scanForConstraints(prop)
|
||||
is FormattedSchema -> scanForConstraints(prop)
|
||||
is FreeFormSchema -> this // todo anything here?
|
||||
is ObjectSchema -> scanForConstraints(clazz, prop)
|
||||
is SimpleSchema -> scanForConstraints(prop)
|
||||
is ReferencedSchema -> this // todo anything here?
|
||||
}
|
||||
|
||||
fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
|
||||
val minItems = prop.findAnnotation<MinItems>()
|
||||
val maxItems = prop.findAnnotation<MaxItems>()
|
||||
val uniqueItems = prop.findAnnotation<UniqueItems>()
|
||||
|
||||
return this.copy(
|
||||
minItems = minItems?.items,
|
||||
maxItems = maxItems?.items,
|
||||
uniqueItems = uniqueItems?.let { true }
|
||||
)
|
||||
}
|
||||
|
||||
fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
return this.copy(nullable = true)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
|
||||
val minimum = prop.findAnnotation<Minimum>()
|
||||
val maximum = prop.findAnnotation<Maximum>()
|
||||
val multipleOf = prop.findAnnotation<MultipleOf>()
|
||||
|
||||
var schema = this
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema.copy(
|
||||
minimum = minimum?.min?.toNumber(),
|
||||
maximum = maximum?.max?.toNumber(),
|
||||
exclusiveMinimum = minimum?.exclusive,
|
||||
exclusiveMaximum = maximum?.exclusive,
|
||||
multipleOf = multipleOf?.multiple?.toNumber(),
|
||||
)
|
||||
}
|
||||
|
||||
fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
|
||||
val minLength = prop.findAnnotation<MinLength>()
|
||||
val maxLength = prop.findAnnotation<MaxLength>()
|
||||
val pattern = prop.findAnnotation<Pattern>()
|
||||
val format = prop.findAnnotation<Format>()
|
||||
|
||||
var schema = this
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema.copy(
|
||||
minLength = minLength?.length,
|
||||
maxLength = maxLength?.length,
|
||||
pattern = pattern?.pattern,
|
||||
format = format?.format
|
||||
)
|
||||
}
|
||||
|
||||
fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
|
||||
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
|
||||
var schema = this
|
||||
|
||||
// todo dedup this
|
||||
if (requiredParams.isNotEmpty()) {
|
||||
schema = schema.copy(required = requiredParams.map { param ->
|
||||
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
|
||||
?.let { field -> field.name.ifBlank { param.name!! } }
|
||||
?: param.name!!
|
||||
})
|
||||
}
|
||||
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
@ -2,4 +2,4 @@ package io.bkbn.kompendium.core.metadata
|
||||
|
||||
import io.bkbn.kompendium.oas.schema.ComponentSchema
|
||||
|
||||
typealias SchemaMap = Map<String, ComponentSchema>
|
||||
typealias SchemaMap = MutableMap<String, ComponentSchema>
|
||||
|
@ -77,7 +77,7 @@ interface IMethodParser {
|
||||
exceptionInfo: Set<ExceptionInfo<*>>,
|
||||
feature: Kompendium,
|
||||
): Map<Int, Response> = exceptionInfo.associate { info ->
|
||||
feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache)
|
||||
Kontent.generateKontent(info.responseType, feature.config.cache)
|
||||
val response = Response(
|
||||
description = info.description,
|
||||
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
|
||||
|
@ -0,0 +1,45 @@
|
||||
package io.bkbn.kompendium.core.schema
|
||||
|
||||
import io.bkbn.kompendium.core.Kontent
|
||||
import io.bkbn.kompendium.core.Kontent.generateKTypeKontent
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.core.util.Helpers
|
||||
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.oas.schema.AnyOfSchema
|
||||
import io.bkbn.kompendium.oas.schema.ArraySchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object CollectionHandler : SchemaHandler {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
/**
|
||||
* Handler for when a [Collection] is encountered
|
||||
* @param type Collection type information
|
||||
* @param clazz Collection class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
|
||||
logger.debug("Collection detected for $type, generating schema and appending to cache")
|
||||
val collectionType = type.arguments.first().type!!
|
||||
val collectionClass = collectionType.classifier as KClass<*>
|
||||
logger.debug("Obtained collection class: $collectionClass")
|
||||
val referenceName = Helpers.genericNameAdapter(type, clazz)
|
||||
generateKTypeKontent(collectionType, cache)
|
||||
val valueReference = when (collectionClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(collectionType)
|
||||
AnyOfSchema(subTypes.map {
|
||||
generateKTypeKontent(it, cache)
|
||||
cache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||
})
|
||||
}
|
||||
false -> cache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
|
||||
}
|
||||
val schema = ArraySchema(items = valueReference)
|
||||
Kontent.generateKontent(collectionType, cache)
|
||||
cache[referenceName] = schema
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package io.bkbn.kompendium.core.schema
|
||||
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.oas.schema.EnumSchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
|
||||
object EnumHandler : SchemaHandler {
|
||||
|
||||
/**
|
||||
* Handler for when an [Enum] is encountered
|
||||
* @param type Map type information
|
||||
* @param clazz Class of the object to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
|
||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||
cache[clazz.simpleName!!] = EnumSchema(options)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package io.bkbn.kompendium.core.schema
|
||||
|
||||
import io.bkbn.kompendium.core.Kontent.generateKTypeKontent
|
||||
import io.bkbn.kompendium.core.Kontent.generateKontent
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
|
||||
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.oas.schema.AnyOfSchema
|
||||
import io.bkbn.kompendium.oas.schema.DictionarySchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object MapHandler : SchemaHandler {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
/**
|
||||
* Handler for when a [Map] is encountered
|
||||
* @param type Map type information
|
||||
* @param clazz Map class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
|
||||
logger.debug("Map detected for $type, generating schema and appending to cache")
|
||||
val (keyType, valType) = type.arguments.map { it.type }
|
||||
logger.debug("Obtained map types -> key: $keyType and value: $valType")
|
||||
if (keyType?.classifier != String::class) {
|
||||
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
|
||||
}
|
||||
generateKTypeKontent(valType!!, cache)
|
||||
val valClass = valType.classifier as KClass<*>
|
||||
val valClassName = valClass.simpleName
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
val valueReference = when (valClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(valType)
|
||||
AnyOfSchema(subTypes.map {
|
||||
generateKTypeKontent(it, cache)
|
||||
cache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||
})
|
||||
}
|
||||
false -> cache[valClassName] ?: error("$valClassName not found")
|
||||
}
|
||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||
generateKontent(valType, cache)
|
||||
cache[referenceName] = schema
|
||||
}
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
package io.bkbn.kompendium.core.schema
|
||||
|
||||
import io.bkbn.kompendium.annotations.Field
|
||||
import io.bkbn.kompendium.annotations.FreeFormObject
|
||||
import io.bkbn.kompendium.annotations.Referenced
|
||||
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||
import io.bkbn.kompendium.annotations.constraint.MaxProperties
|
||||
import io.bkbn.kompendium.annotations.constraint.MinProperties
|
||||
import io.bkbn.kompendium.core.Kontent
|
||||
import io.bkbn.kompendium.core.Kontent.generateKontent
|
||||
import io.bkbn.kompendium.core.constraint.scanForConstraints
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.core.metadata.TypeMap
|
||||
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
|
||||
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.oas.schema.AnyOfSchema
|
||||
import io.bkbn.kompendium.oas.schema.ComponentSchema
|
||||
import io.bkbn.kompendium.oas.schema.FreeFormSchema
|
||||
import io.bkbn.kompendium.oas.schema.ObjectSchema
|
||||
import io.bkbn.kompendium.oas.schema.ReferencedSchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KClassifier
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object ObjectHandler : SchemaHandler {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
/**
|
||||
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
|
||||
* @param type Map type information
|
||||
* @param clazz Class of the object to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
|
||||
// This needs to be simple because it will be stored under its appropriate reference component implicitly
|
||||
val slug = type.getSimpleSlug()
|
||||
// Only analyze if component has not already been stored in the cache
|
||||
if (!cache.containsKey(slug)) {
|
||||
logger.debug("$slug was not found in cache, generating now")
|
||||
// TODO Replace with something better!
|
||||
// If referenced, add tie from simple slug to schema slug
|
||||
if (clazz.hasAnnotation<Referenced>()) {
|
||||
cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug())
|
||||
}
|
||||
// Grabs any type parameters mapped to the corresponding type argument(s)
|
||||
val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
|
||||
// associates each member with a Pair of prop name to property schema
|
||||
val fieldMap = clazz.memberProperties.associate { prop ->
|
||||
logger.debug("Analyzing $prop in class $clazz")
|
||||
|
||||
// Grab the field of the current property
|
||||
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
||||
|
||||
// Short circuit if data is free form
|
||||
val freeForm = prop.findAnnotation<FreeFormObject>()
|
||||
var name = prop.name
|
||||
|
||||
when (freeForm) {
|
||||
null -> {
|
||||
val bleh = handleDefault(typeMap, prop, cache)
|
||||
bleh.first
|
||||
}
|
||||
else -> handleFreeForm(prop)
|
||||
}
|
||||
}
|
||||
logger.debug("Looking for undeclared fields")
|
||||
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
|
||||
val undeclaredType = it.clazz.createType()
|
||||
generateKontent(undeclaredType, cache)
|
||||
it.field to cache[undeclaredType.getSimpleSlug()]!!
|
||||
}
|
||||
logger.debug("$slug contains $fieldMap")
|
||||
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
|
||||
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
|
||||
// todo de-dup this logic
|
||||
if (requiredParams.isNotEmpty()) {
|
||||
schema = schema.copy(required = requiredParams.map { param ->
|
||||
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
|
||||
?.let { field -> field.name.ifBlank { param.name!! } }
|
||||
?: param.name!!
|
||||
})
|
||||
}
|
||||
logger.debug("$slug schema: $schema")
|
||||
cache[slug] = schema
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Better type, or just make map mutable
|
||||
private fun handleDefault(
|
||||
typeMap: TypeMap,
|
||||
prop: KProperty1<*, *>,
|
||||
cache: SchemaMap
|
||||
): Pair<Pair<String, ComponentSchema>, SchemaMap> {
|
||||
var name = prop.name
|
||||
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
||||
val baseType = scanForGeneric(typeMap, prop)
|
||||
val baseClazz = baseType.classifier as KClass<*>
|
||||
val allTypes = scanForSealed(baseClazz, baseType)
|
||||
updateCache(cache, field, allTypes)
|
||||
var propSchema = constructComponentSchema(
|
||||
typeMap = typeMap,
|
||||
prop = prop,
|
||||
fieldClazz = field,
|
||||
clazz = baseClazz,
|
||||
type = baseType,
|
||||
cache = cache
|
||||
)
|
||||
// todo move to helper
|
||||
prop.findAnnotation<Field>()?.let { fieldOverrides ->
|
||||
if (fieldOverrides.description.isNotBlank()) {
|
||||
propSchema = propSchema.setDescription(fieldOverrides.description)
|
||||
}
|
||||
if (fieldOverrides.name.isNotBlank()) {
|
||||
name = fieldOverrides.name
|
||||
}
|
||||
}
|
||||
return Pair(Pair(name, propSchema), cache)
|
||||
}
|
||||
|
||||
private fun handleFreeForm(prop: KProperty1<*, *>): Pair<String, FreeFormSchema> {
|
||||
val minProperties = prop.findAnnotation<MinProperties>()
|
||||
val maxProperties = prop.findAnnotation<MaxProperties>()
|
||||
val schema = FreeFormSchema(
|
||||
minProperties = minProperties?.properties,
|
||||
maxProperties = maxProperties?.properties
|
||||
)
|
||||
return Pair(prop.name, schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Yoinks any generic types from the type map should the field be a generic
|
||||
*/
|
||||
private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
|
||||
if (typeMap.containsKey(prop.returnType.classifier)) {
|
||||
logger.debug("Generic type detected")
|
||||
typeMap[prop.returnType.classifier]?.type!!
|
||||
} else {
|
||||
prop.returnType
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a class for sealed subclasses. If found, returns a list with all children. Otherwise, returns
|
||||
* the base type
|
||||
*/
|
||||
private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
|
||||
clazz.sealedSubclasses.map { it.createType(type.arguments) }
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the type information provided and adds any missing data to the schema map
|
||||
*/
|
||||
private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>) {
|
||||
if (!cache.containsKey(clazz.simpleName)) {
|
||||
logger.debug("Cache was missing ${clazz.simpleName}, adding now")
|
||||
types.forEach {
|
||||
Kontent.generateKTypeKontent(it, cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructComponentSchema(
|
||||
typeMap: TypeMap,
|
||||
clazz: KClass<*>,
|
||||
fieldClazz: KClass<*>,
|
||||
prop: KProperty1<*, *>,
|
||||
type: KType,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema =
|
||||
when (typeMap.containsKey(prop.returnType.classifier)) {
|
||||
true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
|
||||
false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
|
||||
}.scanForConstraints(clazz, prop)
|
||||
|
||||
/**
|
||||
* If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
|
||||
*/
|
||||
private fun handleGenericProperty(
|
||||
typeMap: TypeMap,
|
||||
clazz: KClass<*>,
|
||||
type: KType,
|
||||
classifier: KClassifier?,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema = if (clazz.isSealed) {
|
||||
val refs = clazz.sealedSubclasses
|
||||
.map { it.createType(type.arguments) }
|
||||
.map { it.getSimpleSlug() }
|
||||
.map { cache[it] ?: error("$it not available in cache") }
|
||||
AnyOfSchema(refs)
|
||||
} else {
|
||||
val slug = typeMap[classifier]?.type!!.getSimpleSlug()
|
||||
cache[slug] ?: error("$slug not found in cache")
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
|
||||
*/
|
||||
private fun handleStandardProperty(
|
||||
clazz: KClass<*>,
|
||||
fieldClazz: KClass<*>,
|
||||
prop: KProperty1<*, *>,
|
||||
type: KType,
|
||||
cache: SchemaMap
|
||||
): ComponentSchema = if (clazz.isSealed) {
|
||||
val refs = clazz.sealedSubclasses
|
||||
.map { it.createType(type.arguments) }
|
||||
.map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
|
||||
AnyOfSchema(refs)
|
||||
} else {
|
||||
val slug = fieldClazz.getSimpleSlug(prop)
|
||||
cache[slug] ?: error("$slug not found in cache")
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.bkbn.kompendium.core.schema
|
||||
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
|
||||
interface SchemaHandler {
|
||||
fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap)
|
||||
|
||||
fun gatherSubTypes(type: KType): List<KType> {
|
||||
val classifier = type.classifier as KClass<*>
|
||||
return if (classifier.isSealed) {
|
||||
classifier.sealedSubclasses.map {
|
||||
it.createType(type.arguments)
|
||||
}
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import io.bkbn.kompendium.core.fixtures.TestSimpleWithEnums
|
||||
import io.bkbn.kompendium.core.fixtures.TestSimpleWithList
|
||||
import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap
|
||||
import io.bkbn.kompendium.core.fixtures.TestWithUUID
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.oas.schema.DictionarySchema
|
||||
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
||||
import io.bkbn.kompendium.oas.schema.ObjectSchema
|
||||
@ -21,7 +22,6 @@ import io.kotest.matchers.maps.beEmpty
|
||||
import io.kotest.matchers.maps.shouldContainKey
|
||||
import io.kotest.matchers.maps.shouldHaveKey
|
||||
import io.kotest.matchers.maps.shouldHaveSize
|
||||
import io.kotest.matchers.maps.shouldNotHaveKey
|
||||
import io.kotest.matchers.should
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
@ -30,144 +30,186 @@ import java.util.UUID
|
||||
class KontentTest : DescribeSpec({
|
||||
describe("Kontent analysis") {
|
||||
it("Can return an empty map when passed Unit") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<Unit>()
|
||||
generateKontent<Unit>(cache)
|
||||
|
||||
// assert
|
||||
result should beEmpty()
|
||||
cache should beEmpty()
|
||||
}
|
||||
it("Can return a single map result when analyzing a primitive") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<Long>()
|
||||
generateKontent<Long>(cache)
|
||||
|
||||
// assert
|
||||
result shouldHaveSize 1
|
||||
result["Long"] shouldBe FormattedSchema("int64", "integer")
|
||||
cache shouldHaveSize 1
|
||||
cache["Long"] shouldBe FormattedSchema("int64", "integer")
|
||||
}
|
||||
it("Can handle BigDecimal and BigInteger Types") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestBigNumberModel>()
|
||||
generateKontent<TestBigNumberModel>(cache)
|
||||
|
||||
// assert
|
||||
result shouldHaveSize 3
|
||||
result shouldContainKey TestBigNumberModel::class.simpleName!!
|
||||
result["BigDecimal"] shouldBe FormattedSchema("double", "number")
|
||||
result["BigInteger"] shouldBe FormattedSchema("int64", "integer")
|
||||
cache shouldHaveSize 3
|
||||
cache shouldContainKey TestBigNumberModel::class.simpleName!!
|
||||
cache["BigDecimal"] shouldBe FormattedSchema("double", "number")
|
||||
cache["BigInteger"] shouldBe FormattedSchema("int64", "integer")
|
||||
}
|
||||
it("Can handle ByteArray type") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestByteArrayModel>()
|
||||
generateKontent<TestByteArrayModel>(cache)
|
||||
|
||||
// assert
|
||||
result shouldHaveSize 2
|
||||
result shouldContainKey TestByteArrayModel::class.simpleName!!
|
||||
result["ByteArray"] shouldBe FormattedSchema("byte", "string")
|
||||
cache shouldHaveSize 2
|
||||
cache shouldContainKey TestByteArrayModel::class.simpleName!!
|
||||
cache["ByteArray"] shouldBe FormattedSchema("byte", "string")
|
||||
}
|
||||
it("Allows objects to reference their base type in the cache") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleModel>()
|
||||
generateKontent<TestSimpleModel>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 3
|
||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 3
|
||||
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
}
|
||||
it("Can generate cache for nested object types") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestNestedModel>()
|
||||
generateKontent<TestNestedModel>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 4
|
||||
result shouldContainKey TestNestedModel::class.simpleName!!
|
||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 4
|
||||
cache shouldContainKey TestNestedModel::class.simpleName!!
|
||||
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
}
|
||||
it("Does not repeat generation for cached items") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// arrange
|
||||
val clazz = TestNestedModel::class
|
||||
val initialCache = generateKontent<TestNestedModel>()
|
||||
generateKontent<TestNestedModel>(cache)
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleModel>(initialCache)
|
||||
generateKontent<TestSimpleModel>(cache)
|
||||
|
||||
// assert TODO Spy to check invocation count?
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 4
|
||||
result shouldContainKey clazz.simpleName!!
|
||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 4
|
||||
cache shouldContainKey clazz.simpleName!!
|
||||
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||
}
|
||||
it("allows for generation of enum types") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleWithEnums>()
|
||||
generateKontent<TestSimpleWithEnums>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 3
|
||||
result shouldContainKey TestSimpleWithEnums::class.simpleName!!
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 3
|
||||
cache shouldContainKey TestSimpleWithEnums::class.simpleName!!
|
||||
}
|
||||
it("Allows for generation of map fields") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleWithMap>()
|
||||
generateKontent<TestSimpleWithMap>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 5
|
||||
result shouldContainKey "Map-String-TestSimpleModel"
|
||||
result shouldContainKey TestSimpleWithMap::class.simpleName!!
|
||||
result[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 5
|
||||
cache shouldContainKey "Map-String-TestSimpleModel"
|
||||
cache shouldContainKey TestSimpleWithMap::class.simpleName!!
|
||||
cache[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
|
||||
}
|
||||
it("Throws an error if a map is of an invalid type") {
|
||||
// assert
|
||||
shouldThrow<IllegalStateException> { generateKontent<TestInvalidMap>() }
|
||||
}
|
||||
it("Can generate for collection fields") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleWithList>()
|
||||
generateKontent<TestSimpleWithList>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 6
|
||||
result shouldContainKey "List-TestSimpleModel"
|
||||
result shouldContainKey TestSimpleWithList::class.simpleName!!
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 6
|
||||
cache shouldContainKey "List-TestSimpleModel"
|
||||
cache shouldContainKey TestSimpleWithList::class.simpleName!!
|
||||
}
|
||||
it("Can parse an enum list as a field") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestSimpleWithEnumList>()
|
||||
generateKontent<TestSimpleWithEnumList>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 4
|
||||
result shouldHaveKey "List-SimpleEnum"
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 4
|
||||
cache shouldHaveKey "List-SimpleEnum"
|
||||
}
|
||||
it("Can support UUIDs") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<TestWithUUID>()
|
||||
generateKontent<TestWithUUID>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 2
|
||||
result shouldContainKey UUID::class.simpleName!!
|
||||
result shouldContainKey TestWithUUID::class.simpleName!!
|
||||
result[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 2
|
||||
cache shouldContainKey UUID::class.simpleName!!
|
||||
cache shouldContainKey TestWithUUID::class.simpleName!!
|
||||
cache[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
|
||||
}
|
||||
it("Can generate a top level list response") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<List<TestSimpleModel>>()
|
||||
generateKontent<List<TestSimpleModel>>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 4
|
||||
result shouldContainKey "List-TestSimpleModel"
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 4
|
||||
cache shouldContainKey "List-TestSimpleModel"
|
||||
}
|
||||
it("Can handle a complex type") {
|
||||
// arrange
|
||||
val cache: SchemaMap = mutableMapOf()
|
||||
|
||||
// act
|
||||
val result = generateKontent<ComplexRequest>()
|
||||
generateKontent<ComplexRequest>(cache)
|
||||
|
||||
// assert
|
||||
result shouldNotBe null
|
||||
result shouldHaveSize 7
|
||||
result shouldContainKey "Map-String-CrazyItem"
|
||||
result["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
|
||||
cache shouldNotBe null
|
||||
cache shouldHaveSize 7
|
||||
cache shouldContainKey "Map-String-CrazyItem"
|
||||
cache["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user