chore: handler refactor (#179)

This commit is contained in:
Ryan Brink
2022-02-08 11:41:59 -05:00
committed by GitHub
parent 2788be53ce
commit d9cde5b0d8
13 changed files with 620 additions and 475 deletions

View File

@ -5,6 +5,7 @@
### Added ### Added
### Changed ### Changed
- Cleaned up and broke out handlers into separate classes
### Remove ### Remove

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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