feat: enable format support on type definitions
This commit is contained in:

committed by
GitHub

parent
d2aa6e84d2
commit
7cee839119
@ -1,16 +1,18 @@
|
||||
package io.bkbn.kompendium.core
|
||||
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.annotations.constraint.Format
|
||||
import io.bkbn.kompendium.core.handler.CollectionHandler
|
||||
import io.bkbn.kompendium.core.handler.EnumHandler
|
||||
import io.bkbn.kompendium.core.handler.MapHandler
|
||||
import io.bkbn.kompendium.core.handler.ObjectHandler
|
||||
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||
import io.bkbn.kompendium.core.util.Helpers.logged
|
||||
import io.bkbn.kompendium.oas.schema.FormattedSchema
|
||||
import io.bkbn.kompendium.oas.schema.SimpleSchema
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.typeOf
|
||||
import org.slf4j.LoggerFactory
|
||||
|
@ -24,32 +24,42 @@ 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.KType
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
|
||||
fun ComponentSchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): ComponentSchema =
|
||||
when (this) {
|
||||
is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
|
||||
is ArraySchema -> scanForConstraints(prop)
|
||||
is AnyOfSchema -> scanForConstraints(type, prop)
|
||||
is ArraySchema -> scanForConstraints(type, prop)
|
||||
is DictionarySchema -> this // TODO Anything here?
|
||||
is EnumSchema -> scanForConstraints(prop)
|
||||
is FormattedSchema -> scanForConstraints(prop)
|
||||
is FormattedSchema -> scanForConstraints(type, prop)
|
||||
is FreeFormSchema -> this // todo anything here?
|
||||
is ObjectSchema -> scanForConstraints(clazz, prop)
|
||||
is SimpleSchema -> scanForConstraints(prop)
|
||||
is ObjectSchema -> scanForConstraints(type, prop)
|
||||
is SimpleSchema -> scanForConstraints(type, 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>()
|
||||
fun AnyOfSchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): AnyOfSchema {
|
||||
val anyOf = anyOf.map { it.scanForConstraints(type, prop) }
|
||||
return this.copy(
|
||||
anyOf = anyOf
|
||||
)
|
||||
}
|
||||
|
||||
fun ArraySchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): ArraySchema {
|
||||
val minItems = prop.findAnnotation<MinItems>()?.items ?: this.minItems
|
||||
val maxItems = prop.findAnnotation<MaxItems>()?.items ?: this.maxItems
|
||||
val uniqueItems = prop.findAnnotation<UniqueItems>()?.let { true } ?: this.uniqueItems
|
||||
val items = items.scanForConstraints(type, prop)
|
||||
|
||||
return this.copy(
|
||||
minItems = minItems?.items,
|
||||
maxItems = maxItems?.items,
|
||||
uniqueItems = uniqueItems?.let { true }
|
||||
minItems = minItems,
|
||||
maxItems = maxItems,
|
||||
uniqueItems = uniqueItems,
|
||||
items = items
|
||||
)
|
||||
}
|
||||
|
||||
@ -61,43 +71,46 @@ fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
|
||||
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>()?.length ?: this.minLength
|
||||
val maxLength = prop.findAnnotation<MaxLength>()?.length ?: this.maxLength
|
||||
val pattern = prop.findAnnotation<Pattern>()?.pattern ?: this.pattern
|
||||
val format = prop.findAnnotation<Format>()?.format ?: this.format
|
||||
fun FormattedSchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): FormattedSchema {
|
||||
val minimum = prop.findAnnotation<Minimum>()?.min?.toNumber() ?: this.minimum
|
||||
val exclusiveMinimum = prop.findAnnotation<Minimum>()?.exclusive ?: this.exclusiveMinimum
|
||||
val maximum = prop.findAnnotation<Maximum>()?.max?.toNumber() ?: this.maximum
|
||||
val exclusiveMaximum = prop.findAnnotation<Maximum>()?.exclusive ?: this.exclusiveMaximum
|
||||
val multipleOf = prop.findAnnotation<MultipleOf>()?.multiple?.toNumber() ?: this.multipleOf
|
||||
val format = type.arguments.firstOrNull()?.type?.findAnnotation<Format>()?.format
|
||||
?: prop.findAnnotation<Format>()?.format ?: this.format
|
||||
val nullable = if (prop.returnType.isMarkedNullable) true else this.nullable
|
||||
|
||||
return copy(
|
||||
return this.copy(
|
||||
minimum = minimum,
|
||||
maximum = maximum,
|
||||
exclusiveMinimum = exclusiveMinimum,
|
||||
exclusiveMaximum = exclusiveMaximum,
|
||||
multipleOf = multipleOf,
|
||||
nullable = nullable,
|
||||
minLength = minLength,
|
||||
maxLength = maxLength,
|
||||
pattern = pattern,
|
||||
format = format
|
||||
)
|
||||
}
|
||||
|
||||
fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
|
||||
fun SimpleSchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): SimpleSchema {
|
||||
val minLength = prop.findAnnotation<MinLength>()?.length ?: this.minLength
|
||||
val maxLength = prop.findAnnotation<MaxLength>()?.length ?: this.maxLength
|
||||
val pattern = prop.findAnnotation<Pattern>()?.pattern ?: this.pattern
|
||||
val format = type.arguments.firstOrNull()?.type?.findAnnotation<Format>()?.format
|
||||
?: prop.findAnnotation<Format>()?.format ?: this.format
|
||||
val nullable = if (prop.returnType.isMarkedNullable) true else this.nullable
|
||||
|
||||
return this.copy(
|
||||
minLength = minLength,
|
||||
maxLength = maxLength,
|
||||
pattern = pattern,
|
||||
format = format,
|
||||
nullable = nullable
|
||||
)
|
||||
}
|
||||
|
||||
fun ObjectSchema.scanForConstraints(type: KType, prop: KProperty1<*, *>): ObjectSchema {
|
||||
val clazz = type.classifier as KClass<*>
|
||||
var schema = this.adjustForRequiredParams(clazz)
|
||||
if (prop.returnType.isMarkedNullable) {
|
||||
schema = schema.copy(nullable = true)
|
||||
|
@ -168,7 +168,7 @@ object ObjectHandler : SchemaHandler {
|
||||
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)
|
||||
}.scanForConstraints(type, prop)
|
||||
|
||||
/**
|
||||
* If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
|
||||
|
@ -17,6 +17,7 @@ import io.bkbn.kompendium.core.util.defaultParameter
|
||||
import io.bkbn.kompendium.core.util.exampleParams
|
||||
import io.bkbn.kompendium.core.util.exclusiveMinMax
|
||||
import io.bkbn.kompendium.core.util.formattedParam
|
||||
import io.bkbn.kompendium.core.util.formattedType
|
||||
import io.bkbn.kompendium.core.util.freeFormObject
|
||||
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
|
||||
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
|
||||
@ -278,6 +279,9 @@ class KompendiumTest : DescribeSpec({
|
||||
it("Can set a minimum and maximum number of properties on a free-form type") {
|
||||
openApiTestAllSerializers("min_max_free_form.json") { minMaxFreeForm() }
|
||||
}
|
||||
it("Can add a custom format to a collection type") {
|
||||
openApiTestAllSerializers("formatted_array_item_type.json") { formattedType() }
|
||||
}
|
||||
}
|
||||
describe("Formats") {
|
||||
it("Can set a format on a simple type schema") {
|
||||
|
@ -608,3 +608,13 @@ fun Application.exampleParams() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.formattedType() {
|
||||
routing {
|
||||
route("/test/formatted_type") {
|
||||
notarizedPost(TestResponseInfo.formattedArrayItemType) {
|
||||
call.respond(HttpStatusCode.OK, TestResponse("hi"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Test API",
|
||||
"version": "1.33.7",
|
||||
"description": "An amazing, fully-ish 😉 generated API spec",
|
||||
"termsOfService": "https://example.com",
|
||||
"contact": {
|
||||
"name": "Homer Simpson",
|
||||
"url": "https://gph.is/1NPUDiM",
|
||||
"email": "chunkylover53@aol.com"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://myawesomeapi.com",
|
||||
"description": "Production instance of my API"
|
||||
},
|
||||
{
|
||||
"url": "https://staging.myawesomeapi.com",
|
||||
"description": "Where the fun stuff happens"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/test/formatted_type": {
|
||||
"post": {
|
||||
"tags": [],
|
||||
"summary": "formatted array item type",
|
||||
"description": "Cool stuff",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"description": "cool",
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FormattedArrayItemType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful endeavor",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"FormattedArrayItemType": {
|
||||
"properties": {
|
||||
"a": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"a"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TestResponse": {
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"security": [],
|
||||
"tags": []
|
||||
}
|
@ -96,6 +96,10 @@ data class FormattedString(
|
||||
val a: String
|
||||
)
|
||||
|
||||
data class FormattedArrayItemType(
|
||||
val a: List<@Format("binary") String>
|
||||
)
|
||||
|
||||
data class MinMaxString(
|
||||
@MinLength(42)
|
||||
@MaxLength(1337)
|
||||
|
@ -11,6 +11,7 @@ import io.bkbn.kompendium.core.metadata.method.OptionsInfo
|
||||
import io.bkbn.kompendium.core.metadata.method.PatchInfo
|
||||
import io.bkbn.kompendium.core.metadata.method.PostInfo
|
||||
import io.bkbn.kompendium.core.metadata.method.PutInfo
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@ -300,5 +301,13 @@ object TestResponseInfo {
|
||||
)
|
||||
)
|
||||
|
||||
val formattedArrayItemType = PostInfo<Unit, FormattedArrayItemType, TestResponse>(
|
||||
summary = "formatted array item type",
|
||||
description = "Cool stuff",
|
||||
responseInfo = simpleOkResponse(),
|
||||
requestInfo = RequestInfo<FormattedArrayItemType>("cool")
|
||||
.copy(mediaTypes = listOf(ContentType.MultiPart.FormData.toString()))
|
||||
)
|
||||
|
||||
private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor")
|
||||
}
|
||||
|
Reference in New Issue
Block a user