diff --git a/CHANGELOG.md b/CHANGELOG.md index 68eee7875..6b97d5e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added ### Changed +- Cleaned up and broke out handlers into separate classes ### Remove diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt index 19c0eba28..7c7a527c0 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt @@ -18,12 +18,12 @@ class Kompendium(val config: Configuration) { class Configuration { lateinit var spec: OpenApiSpec - var cache: SchemaMap = emptyMap() + var cache: SchemaMap = mutableMapOf() var specRoute = "/openapi.json" // TODO Add tests for this!! fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) { - cache = cache.plus(clazz.simpleName!! to schema) + cache[clazz.simpleName!!] = schema } } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt index ee094e201..a693a4e78 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt @@ -43,9 +43,9 @@ object KompendiumPreFlight { } fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) { - feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache) - feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache) - feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache) + Kontent.generateKontent(requestType, feature.config.cache) + Kontent.generateKontent(responseType, feature.config.cache) + Kontent.generateKontent(paramType, feature.config.cache) feature.updateReferences() } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt index b1289f08d..7f4883eb3 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt @@ -1,49 +1,17 @@ package io.bkbn.kompendium.core -import io.bkbn.kompendium.annotations.Field -import io.bkbn.kompendium.annotations.FreeFormObject -import io.bkbn.kompendium.annotations.Referenced -import io.bkbn.kompendium.annotations.UndeclaredField -import io.bkbn.kompendium.annotations.constraint.Format -import io.bkbn.kompendium.annotations.constraint.MaxItems -import io.bkbn.kompendium.annotations.constraint.MaxLength -import io.bkbn.kompendium.annotations.constraint.MaxProperties -import io.bkbn.kompendium.annotations.constraint.Maximum -import io.bkbn.kompendium.annotations.constraint.MinItems -import io.bkbn.kompendium.annotations.constraint.MinLength -import io.bkbn.kompendium.annotations.constraint.MinProperties -import io.bkbn.kompendium.annotations.constraint.Minimum -import io.bkbn.kompendium.annotations.constraint.MultipleOf -import io.bkbn.kompendium.annotations.constraint.Pattern -import io.bkbn.kompendium.annotations.constraint.UniqueItems import io.bkbn.kompendium.core.metadata.SchemaMap -import io.bkbn.kompendium.core.metadata.TypeMap -import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter -import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug -import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug +import io.bkbn.kompendium.core.schema.CollectionHandler +import io.bkbn.kompendium.core.schema.EnumHandler +import io.bkbn.kompendium.core.schema.MapHandler +import io.bkbn.kompendium.core.schema.ObjectHandler import io.bkbn.kompendium.core.util.Helpers.logged -import io.bkbn.kompendium.core.util.Helpers.toNumber -import io.bkbn.kompendium.oas.schema.AnyOfSchema -import io.bkbn.kompendium.oas.schema.ArraySchema -import io.bkbn.kompendium.oas.schema.ComponentSchema -import io.bkbn.kompendium.oas.schema.DictionarySchema -import io.bkbn.kompendium.oas.schema.EnumSchema import io.bkbn.kompendium.oas.schema.FormattedSchema -import io.bkbn.kompendium.oas.schema.FreeFormSchema -import io.bkbn.kompendium.oas.schema.ObjectSchema -import io.bkbn.kompendium.oas.schema.ReferencedSchema import io.bkbn.kompendium.oas.schema.SimpleSchema import kotlin.reflect.KClass -import kotlin.reflect.KClassifier -import kotlin.reflect.KProperty1 import kotlin.reflect.KType import kotlin.reflect.full.createType -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaField import kotlin.reflect.typeOf import org.slf4j.LoggerFactory import java.math.BigDecimal @@ -65,10 +33,10 @@ object Kontent { */ @OptIn(ExperimentalStdlibApi::class) inline fun generateKontent( - cache: SchemaMap = emptyMap() - ): SchemaMap { + cache: SchemaMap = mutableMapOf() + ) { val kontentType = typeOf() - return generateKTypeKontent(kontentType, cache) + generateKTypeKontent(kontentType, cache) } /** @@ -79,13 +47,11 @@ object Kontent { */ fun generateKontent( type: KType, - cache: SchemaMap = emptyMap() - ): SchemaMap { - var newCache = cache + cache: SchemaMap = mutableMapOf() + ) { gatherSubTypes(type).forEach { - newCache = generateKTypeKontent(it, newCache) + generateKTypeKontent(it, cache) } - return newCache } private fun gatherSubTypes(type: KType): List { @@ -106,371 +72,27 @@ object Kontent { */ fun generateKTypeKontent( type: KType, - cache: SchemaMap = emptyMap(), - ): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) { + cache: SchemaMap = mutableMapOf(), + ) = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) { logger.debug("Parsing Kontent of $type") when (val clazz = type.classifier as KClass<*>) { Unit::class -> cache - Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer")) - Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer")) - Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number")) - Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number")) - String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string")) - Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean")) - UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string")) - BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number")) - BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer")) - ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string")) + Int::class -> cache[clazz.simpleName!!] = FormattedSchema("int32", "integer") + Long::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer") + Double::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number") + Float::class -> cache[clazz.simpleName!!] = FormattedSchema("float", "number") + String::class -> cache[clazz.simpleName!!] = SimpleSchema("string") + Boolean::class -> cache[clazz.simpleName!!] = SimpleSchema("boolean") + UUID::class -> cache[clazz.simpleName!!] = FormattedSchema("uuid", "string") + BigDecimal::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number") + BigInteger::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer") + ByteArray::class -> cache[clazz.simpleName!!] = FormattedSchema("byte", "string") else -> when { - clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache) - clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache) - clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache) - else -> handleComplexType(type, clazz, cache) + clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, clazz, cache) + clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache) + clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, clazz, cache) + else -> ObjectHandler.handle(type, clazz, cache) } } } - - /** - * In the event of an object type, this method will parse out individual fields to recursively aggregate object map. - * @param clazz Class of the object to analyze - * @param cache Existing schema map to append to - */ - @Suppress("LongMethod", "ComplexMethod") - private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap { - // This needs to be simple because it will be stored under its appropriate reference component implicitly - val slug = type.getSimpleSlug() - // Only analyze if component has not already been stored in the cache - return when (cache.containsKey(slug)) { - true -> { - logger.debug("Cache already contains $slug, returning cache untouched") - cache - } - false -> { - logger.debug("$slug was not found in cache, generating now") - var newCache = cache - // If referenced, add tie from simple slug to schema slug - if (clazz.hasAnnotation()) { - 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() - 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()?.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() - val maxProperties = prop.findAnnotation() - val schema = - FreeFormSchema(minProperties = minProperties?.properties, maxProperties = maxProperties?.properties) - Pair(name, schema) - } - } - } - logger.debug("Looking for undeclared fields") - val undeclaredFieldMap = clazz.annotations.filterIsInstance().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() - ?.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): 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 = 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() - val maxItems = prop.findAnnotation() - val uniqueItems = prop.findAnnotation() - - 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() - val maximum = prop.findAnnotation() - val multipleOf = prop.findAnnotation() - - 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() - val maxLength = prop.findAnnotation() - val pattern = prop.findAnnotation() - val format = prop.findAnnotation() - - 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() - ?.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) - } } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt new file mode 100644 index 000000000..31ff7f06e --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt @@ -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() + val maxItems = prop.findAnnotation() + val uniqueItems = prop.findAnnotation() + + 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() + val maximum = prop.findAnnotation() + val multipleOf = prop.findAnnotation() + + 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() + val maxLength = prop.findAnnotation() + val pattern = prop.findAnnotation() + val format = prop.findAnnotation() + + 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() + ?.let { field -> field.name.ifBlank { param.name!! } } + ?: param.name!! + }) + } + + if (prop.returnType.isMarkedNullable) { + schema = schema.copy(nullable = true) + } + + return schema +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt index 92889ca6e..22c016ca1 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt @@ -2,4 +2,4 @@ package io.bkbn.kompendium.core.metadata import io.bkbn.kompendium.oas.schema.ComponentSchema -typealias SchemaMap = Map +typealias SchemaMap = MutableMap diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt index 0650a82e6..1784aab3a 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt @@ -77,7 +77,7 @@ interface IMethodParser { exceptionInfo: Set>, feature: Kompendium, ): Map = exceptionInfo.associate { info -> - feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache) + Kontent.generateKontent(info.responseType, feature.config.cache) val response = Response( description = info.description, content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples) diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt new file mode 100644 index 000000000..037759e4d --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt @@ -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 + } +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt new file mode 100644 index 000000000..b273df055 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt @@ -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) + } +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt new file mode 100644 index 000000000..cfe65ada3 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt @@ -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 + } +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt new file mode 100644 index 000000000..f4c52ab35 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt @@ -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()) { + 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() + 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().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() + ?.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, 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()?.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 { + val minProperties = prop.findAnnotation() + val maxProperties = prop.findAnnotation() + 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 = 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) { + 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") + } +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt new file mode 100644 index 000000000..98ba19654 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt @@ -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 { + val classifier = type.classifier as KClass<*> + return if (classifier.isSealed) { + classifier.sealedSubclasses.map { + it.createType(type.arguments) + } + } else { + listOf(type) + } + } +} diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt index c2ccf22b9..7cff10847 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt @@ -12,6 +12,7 @@ import io.bkbn.kompendium.core.fixtures.TestSimpleWithEnums import io.bkbn.kompendium.core.fixtures.TestSimpleWithList import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap import io.bkbn.kompendium.core.fixtures.TestWithUUID +import io.bkbn.kompendium.core.metadata.SchemaMap import io.bkbn.kompendium.oas.schema.DictionarySchema import io.bkbn.kompendium.oas.schema.FormattedSchema import io.bkbn.kompendium.oas.schema.ObjectSchema @@ -21,7 +22,6 @@ import io.kotest.matchers.maps.beEmpty import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.maps.shouldHaveKey import io.kotest.matchers.maps.shouldHaveSize -import io.kotest.matchers.maps.shouldNotHaveKey import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -30,144 +30,186 @@ import java.util.UUID class KontentTest : DescribeSpec({ describe("Kontent analysis") { it("Can return an empty map when passed Unit") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result should beEmpty() + cache should beEmpty() } it("Can return a single map result when analyzing a primitive") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldHaveSize 1 - result["Long"] shouldBe FormattedSchema("int64", "integer") + cache shouldHaveSize 1 + cache["Long"] shouldBe FormattedSchema("int64", "integer") } it("Can handle BigDecimal and BigInteger Types") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldHaveSize 3 - result shouldContainKey TestBigNumberModel::class.simpleName!! - result["BigDecimal"] shouldBe FormattedSchema("double", "number") - result["BigInteger"] shouldBe FormattedSchema("int64", "integer") + cache shouldHaveSize 3 + cache shouldContainKey TestBigNumberModel::class.simpleName!! + cache["BigDecimal"] shouldBe FormattedSchema("double", "number") + cache["BigInteger"] shouldBe FormattedSchema("int64", "integer") } it("Can handle ByteArray type") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldHaveSize 2 - result shouldContainKey TestByteArrayModel::class.simpleName!! - result["ByteArray"] shouldBe FormattedSchema("byte", "string") + cache shouldHaveSize 2 + cache shouldContainKey TestByteArrayModel::class.simpleName!! + cache["ByteArray"] shouldBe FormattedSchema("byte", "string") } it("Allows objects to reference their base type in the cache") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 3 - result shouldContainKey TestSimpleModel::class.simpleName!! + cache shouldNotBe null + cache shouldHaveSize 3 + cache shouldContainKey TestSimpleModel::class.simpleName!! } it("Can generate cache for nested object types") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 4 - result shouldContainKey TestNestedModel::class.simpleName!! - result shouldContainKey TestSimpleModel::class.simpleName!! + cache shouldNotBe null + cache shouldHaveSize 4 + cache shouldContainKey TestNestedModel::class.simpleName!! + cache shouldContainKey TestSimpleModel::class.simpleName!! } it("Does not repeat generation for cached items") { + // arrange + val cache: SchemaMap = mutableMapOf() + // arrange val clazz = TestNestedModel::class - val initialCache = generateKontent() + generateKontent(cache) // act - val result = generateKontent(initialCache) + generateKontent(cache) // assert TODO Spy to check invocation count? - result shouldNotBe null - result shouldHaveSize 4 - result shouldContainKey clazz.simpleName!! - result shouldContainKey TestSimpleModel::class.simpleName!! + cache shouldNotBe null + cache shouldHaveSize 4 + cache shouldContainKey clazz.simpleName!! + cache shouldContainKey TestSimpleModel::class.simpleName!! } it("allows for generation of enum types") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 3 - result shouldContainKey TestSimpleWithEnums::class.simpleName!! + cache shouldNotBe null + cache shouldHaveSize 3 + cache shouldContainKey TestSimpleWithEnums::class.simpleName!! } it("Allows for generation of map fields") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 5 - result shouldContainKey "Map-String-TestSimpleModel" - result shouldContainKey TestSimpleWithMap::class.simpleName!! - result[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve + cache shouldNotBe null + cache shouldHaveSize 5 + cache shouldContainKey "Map-String-TestSimpleModel" + cache shouldContainKey TestSimpleWithMap::class.simpleName!! + cache[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve } it("Throws an error if a map is of an invalid type") { // assert shouldThrow { generateKontent() } } it("Can generate for collection fields") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 6 - result shouldContainKey "List-TestSimpleModel" - result shouldContainKey TestSimpleWithList::class.simpleName!! + cache shouldNotBe null + cache shouldHaveSize 6 + cache shouldContainKey "List-TestSimpleModel" + cache shouldContainKey TestSimpleWithList::class.simpleName!! } it("Can parse an enum list as a field") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 4 - result shouldHaveKey "List-SimpleEnum" + cache shouldNotBe null + cache shouldHaveSize 4 + cache shouldHaveKey "List-SimpleEnum" } it("Can support UUIDs") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 2 - result shouldContainKey UUID::class.simpleName!! - result shouldContainKey TestWithUUID::class.simpleName!! - result[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string") + cache shouldNotBe null + cache shouldHaveSize 2 + cache shouldContainKey UUID::class.simpleName!! + cache shouldContainKey TestWithUUID::class.simpleName!! + cache[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string") } it("Can generate a top level list response") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent>() + generateKontent>(cache) // assert - result shouldNotBe null - result shouldHaveSize 4 - result shouldContainKey "List-TestSimpleModel" + cache shouldNotBe null + cache shouldHaveSize 4 + cache shouldContainKey "List-TestSimpleModel" } it("Can handle a complex type") { + // arrange + val cache: SchemaMap = mutableMapOf() + // act - val result = generateKontent() + generateKontent(cache) // assert - result shouldNotBe null - result shouldHaveSize 7 - result shouldContainKey "Map-String-CrazyItem" - result["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null + cache shouldNotBe null + cache shouldHaveSize 7 + cache shouldContainKey "Map-String-CrazyItem" + cache["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null } } })