chore: handler refactor (#179)
This commit is contained in:
@ -5,6 +5,7 @@
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Cleaned up and broke out handlers into separate classes
|
||||||
|
|
||||||
### Remove
|
### Remove
|
||||||
|
|
||||||
|
@ -18,12 +18,12 @@ class Kompendium(val config: Configuration) {
|
|||||||
class Configuration {
|
class Configuration {
|
||||||
lateinit var spec: OpenApiSpec
|
lateinit var spec: OpenApiSpec
|
||||||
|
|
||||||
var cache: SchemaMap = emptyMap()
|
var cache: SchemaMap = mutableMapOf()
|
||||||
var specRoute = "/openapi.json"
|
var specRoute = "/openapi.json"
|
||||||
|
|
||||||
// TODO Add tests for this!!
|
// TODO Add tests for this!!
|
||||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
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) {
|
fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) {
|
||||||
feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
|
Kontent.generateKontent(requestType, feature.config.cache)
|
||||||
feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
|
Kontent.generateKontent(responseType, feature.config.cache)
|
||||||
feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
|
Kontent.generateKontent(paramType, feature.config.cache)
|
||||||
feature.updateReferences()
|
feature.updateReferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,49 +1,17 @@
|
|||||||
package io.bkbn.kompendium.core
|
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.SchemaMap
|
||||||
import io.bkbn.kompendium.core.metadata.TypeMap
|
import io.bkbn.kompendium.core.schema.CollectionHandler
|
||||||
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
|
import io.bkbn.kompendium.core.schema.EnumHandler
|
||||||
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
|
import io.bkbn.kompendium.core.schema.MapHandler
|
||||||
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
import io.bkbn.kompendium.core.schema.ObjectHandler
|
||||||
import io.bkbn.kompendium.core.util.Helpers.logged
|
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.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 io.bkbn.kompendium.oas.schema.SimpleSchema
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KClassifier
|
|
||||||
import kotlin.reflect.KProperty1
|
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
import kotlin.reflect.full.createType
|
import kotlin.reflect.full.createType
|
||||||
import kotlin.reflect.full.findAnnotation
|
|
||||||
import kotlin.reflect.full.hasAnnotation
|
|
||||||
import kotlin.reflect.full.isSubclassOf
|
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 kotlin.reflect.typeOf
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
@ -65,10 +33,10 @@ object Kontent {
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
inline fun <reified T> generateKontent(
|
inline fun <reified T> generateKontent(
|
||||||
cache: SchemaMap = emptyMap()
|
cache: SchemaMap = mutableMapOf()
|
||||||
): SchemaMap {
|
) {
|
||||||
val kontentType = typeOf<T>()
|
val kontentType = typeOf<T>()
|
||||||
return generateKTypeKontent(kontentType, cache)
|
generateKTypeKontent(kontentType, cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,13 +47,11 @@ object Kontent {
|
|||||||
*/
|
*/
|
||||||
fun generateKontent(
|
fun generateKontent(
|
||||||
type: KType,
|
type: KType,
|
||||||
cache: SchemaMap = emptyMap()
|
cache: SchemaMap = mutableMapOf()
|
||||||
): SchemaMap {
|
) {
|
||||||
var newCache = cache
|
|
||||||
gatherSubTypes(type).forEach {
|
gatherSubTypes(type).forEach {
|
||||||
newCache = generateKTypeKontent(it, newCache)
|
generateKTypeKontent(it, cache)
|
||||||
}
|
}
|
||||||
return newCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun gatherSubTypes(type: KType): List<KType> {
|
private fun gatherSubTypes(type: KType): List<KType> {
|
||||||
@ -106,371 +72,27 @@ object Kontent {
|
|||||||
*/
|
*/
|
||||||
fun generateKTypeKontent(
|
fun generateKTypeKontent(
|
||||||
type: KType,
|
type: KType,
|
||||||
cache: SchemaMap = emptyMap(),
|
cache: SchemaMap = mutableMapOf(),
|
||||||
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
) = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||||
logger.debug("Parsing Kontent of $type")
|
logger.debug("Parsing Kontent of $type")
|
||||||
when (val clazz = type.classifier as KClass<*>) {
|
when (val clazz = type.classifier as KClass<*>) {
|
||||||
Unit::class -> cache
|
Unit::class -> cache
|
||||||
Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer"))
|
Int::class -> cache[clazz.simpleName!!] = FormattedSchema("int32", "integer")
|
||||||
Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
Long::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
|
||||||
Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
Double::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
|
||||||
Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number"))
|
Float::class -> cache[clazz.simpleName!!] = FormattedSchema("float", "number")
|
||||||
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
String::class -> cache[clazz.simpleName!!] = SimpleSchema("string")
|
||||||
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
Boolean::class -> cache[clazz.simpleName!!] = SimpleSchema("boolean")
|
||||||
UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string"))
|
UUID::class -> cache[clazz.simpleName!!] = FormattedSchema("uuid", "string")
|
||||||
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
BigDecimal::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
|
||||||
BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
BigInteger::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
|
||||||
ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string"))
|
ByteArray::class -> cache[clazz.simpleName!!] = FormattedSchema("byte", "string")
|
||||||
else -> when {
|
else -> when {
|
||||||
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
|
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, clazz, cache)
|
||||||
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
|
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
|
||||||
clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache)
|
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, clazz, cache)
|
||||||
else -> handleComplexType(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
|
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<*>>,
|
exceptionInfo: Set<ExceptionInfo<*>>,
|
||||||
feature: Kompendium,
|
feature: Kompendium,
|
||||||
): Map<Int, Response> = exceptionInfo.associate { info ->
|
): 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(
|
val response = Response(
|
||||||
description = info.description,
|
description = info.description,
|
||||||
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
|
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.TestSimpleWithList
|
||||||
import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap
|
import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap
|
||||||
import io.bkbn.kompendium.core.fixtures.TestWithUUID
|
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.DictionarySchema
|
||||||
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
||||||
import io.bkbn.kompendium.oas.schema.ObjectSchema
|
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.shouldContainKey
|
||||||
import io.kotest.matchers.maps.shouldHaveKey
|
import io.kotest.matchers.maps.shouldHaveKey
|
||||||
import io.kotest.matchers.maps.shouldHaveSize
|
import io.kotest.matchers.maps.shouldHaveSize
|
||||||
import io.kotest.matchers.maps.shouldNotHaveKey
|
|
||||||
import io.kotest.matchers.should
|
import io.kotest.matchers.should
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.kotest.matchers.shouldNotBe
|
import io.kotest.matchers.shouldNotBe
|
||||||
@ -30,144 +30,186 @@ import java.util.UUID
|
|||||||
class KontentTest : DescribeSpec({
|
class KontentTest : DescribeSpec({
|
||||||
describe("Kontent analysis") {
|
describe("Kontent analysis") {
|
||||||
it("Can return an empty map when passed Unit") {
|
it("Can return an empty map when passed Unit") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<Unit>()
|
generateKontent<Unit>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result should beEmpty()
|
cache should beEmpty()
|
||||||
}
|
}
|
||||||
it("Can return a single map result when analyzing a primitive") {
|
it("Can return a single map result when analyzing a primitive") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<Long>()
|
generateKontent<Long>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldHaveSize 1
|
cache shouldHaveSize 1
|
||||||
result["Long"] shouldBe FormattedSchema("int64", "integer")
|
cache["Long"] shouldBe FormattedSchema("int64", "integer")
|
||||||
}
|
}
|
||||||
it("Can handle BigDecimal and BigInteger Types") {
|
it("Can handle BigDecimal and BigInteger Types") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestBigNumberModel>()
|
generateKontent<TestBigNumberModel>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldHaveSize 3
|
cache shouldHaveSize 3
|
||||||
result shouldContainKey TestBigNumberModel::class.simpleName!!
|
cache shouldContainKey TestBigNumberModel::class.simpleName!!
|
||||||
result["BigDecimal"] shouldBe FormattedSchema("double", "number")
|
cache["BigDecimal"] shouldBe FormattedSchema("double", "number")
|
||||||
result["BigInteger"] shouldBe FormattedSchema("int64", "integer")
|
cache["BigInteger"] shouldBe FormattedSchema("int64", "integer")
|
||||||
}
|
}
|
||||||
it("Can handle ByteArray type") {
|
it("Can handle ByteArray type") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestByteArrayModel>()
|
generateKontent<TestByteArrayModel>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldHaveSize 2
|
cache shouldHaveSize 2
|
||||||
result shouldContainKey TestByteArrayModel::class.simpleName!!
|
cache shouldContainKey TestByteArrayModel::class.simpleName!!
|
||||||
result["ByteArray"] shouldBe FormattedSchema("byte", "string")
|
cache["ByteArray"] shouldBe FormattedSchema("byte", "string")
|
||||||
}
|
}
|
||||||
it("Allows objects to reference their base type in the cache") {
|
it("Allows objects to reference their base type in the cache") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleModel>()
|
generateKontent<TestSimpleModel>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 3
|
cache shouldHaveSize 3
|
||||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||||
}
|
}
|
||||||
it("Can generate cache for nested object types") {
|
it("Can generate cache for nested object types") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestNestedModel>()
|
generateKontent<TestNestedModel>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 4
|
cache shouldHaveSize 4
|
||||||
result shouldContainKey TestNestedModel::class.simpleName!!
|
cache shouldContainKey TestNestedModel::class.simpleName!!
|
||||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||||
}
|
}
|
||||||
it("Does not repeat generation for cached items") {
|
it("Does not repeat generation for cached items") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// arrange
|
// arrange
|
||||||
val clazz = TestNestedModel::class
|
val clazz = TestNestedModel::class
|
||||||
val initialCache = generateKontent<TestNestedModel>()
|
generateKontent<TestNestedModel>(cache)
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleModel>(initialCache)
|
generateKontent<TestSimpleModel>(cache)
|
||||||
|
|
||||||
// assert TODO Spy to check invocation count?
|
// assert TODO Spy to check invocation count?
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 4
|
cache shouldHaveSize 4
|
||||||
result shouldContainKey clazz.simpleName!!
|
cache shouldContainKey clazz.simpleName!!
|
||||||
result shouldContainKey TestSimpleModel::class.simpleName!!
|
cache shouldContainKey TestSimpleModel::class.simpleName!!
|
||||||
}
|
}
|
||||||
it("allows for generation of enum types") {
|
it("allows for generation of enum types") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleWithEnums>()
|
generateKontent<TestSimpleWithEnums>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 3
|
cache shouldHaveSize 3
|
||||||
result shouldContainKey TestSimpleWithEnums::class.simpleName!!
|
cache shouldContainKey TestSimpleWithEnums::class.simpleName!!
|
||||||
}
|
}
|
||||||
it("Allows for generation of map fields") {
|
it("Allows for generation of map fields") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleWithMap>()
|
generateKontent<TestSimpleWithMap>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 5
|
cache shouldHaveSize 5
|
||||||
result shouldContainKey "Map-String-TestSimpleModel"
|
cache shouldContainKey "Map-String-TestSimpleModel"
|
||||||
result shouldContainKey TestSimpleWithMap::class.simpleName!!
|
cache shouldContainKey TestSimpleWithMap::class.simpleName!!
|
||||||
result[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
|
cache[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
|
||||||
}
|
}
|
||||||
it("Throws an error if a map is of an invalid type") {
|
it("Throws an error if a map is of an invalid type") {
|
||||||
// assert
|
// assert
|
||||||
shouldThrow<IllegalStateException> { generateKontent<TestInvalidMap>() }
|
shouldThrow<IllegalStateException> { generateKontent<TestInvalidMap>() }
|
||||||
}
|
}
|
||||||
it("Can generate for collection fields") {
|
it("Can generate for collection fields") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleWithList>()
|
generateKontent<TestSimpleWithList>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 6
|
cache shouldHaveSize 6
|
||||||
result shouldContainKey "List-TestSimpleModel"
|
cache shouldContainKey "List-TestSimpleModel"
|
||||||
result shouldContainKey TestSimpleWithList::class.simpleName!!
|
cache shouldContainKey TestSimpleWithList::class.simpleName!!
|
||||||
}
|
}
|
||||||
it("Can parse an enum list as a field") {
|
it("Can parse an enum list as a field") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestSimpleWithEnumList>()
|
generateKontent<TestSimpleWithEnumList>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 4
|
cache shouldHaveSize 4
|
||||||
result shouldHaveKey "List-SimpleEnum"
|
cache shouldHaveKey "List-SimpleEnum"
|
||||||
}
|
}
|
||||||
it("Can support UUIDs") {
|
it("Can support UUIDs") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<TestWithUUID>()
|
generateKontent<TestWithUUID>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 2
|
cache shouldHaveSize 2
|
||||||
result shouldContainKey UUID::class.simpleName!!
|
cache shouldContainKey UUID::class.simpleName!!
|
||||||
result shouldContainKey TestWithUUID::class.simpleName!!
|
cache shouldContainKey TestWithUUID::class.simpleName!!
|
||||||
result[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
|
cache[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
|
||||||
}
|
}
|
||||||
it("Can generate a top level list response") {
|
it("Can generate a top level list response") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<List<TestSimpleModel>>()
|
generateKontent<List<TestSimpleModel>>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 4
|
cache shouldHaveSize 4
|
||||||
result shouldContainKey "List-TestSimpleModel"
|
cache shouldContainKey "List-TestSimpleModel"
|
||||||
}
|
}
|
||||||
it("Can handle a complex type") {
|
it("Can handle a complex type") {
|
||||||
|
// arrange
|
||||||
|
val cache: SchemaMap = mutableMapOf()
|
||||||
|
|
||||||
// act
|
// act
|
||||||
val result = generateKontent<ComplexRequest>()
|
generateKontent<ComplexRequest>(cache)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
result shouldNotBe null
|
cache shouldNotBe null
|
||||||
result shouldHaveSize 7
|
cache shouldHaveSize 7
|
||||||
result shouldContainKey "Map-String-CrazyItem"
|
cache shouldContainKey "Map-String-CrazyItem"
|
||||||
result["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
|
cache["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user