feat: enable format support on type definitions

This commit is contained in:
Gennadi Kudrjavtsev
2022-02-25 16:30:18 +02:00
committed by GitHub
parent d2aa6e84d2
commit 7cee839119
10 changed files with 184 additions and 46 deletions

View File

@ -3,6 +3,7 @@
## Unreleased
- Fixed support Location classes located in other non-location classes
- Fixed formatting of a custom SimpleSchema
- Multipart form-data multiple file request support
### Added

View File

@ -1,5 +1,5 @@
package io.bkbn.kompendium.annotations.constraint
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
annotation class Format(val format: String)

View File

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

View File

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

View File

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

View File

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

View File

@ -608,3 +608,13 @@ fun Application.exampleParams() {
}
}
}
fun Application.formattedType() {
routing {
route("/test/formatted_type") {
notarizedPost(TestResponseInfo.formattedArrayItemType) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}

View File

@ -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": []
}

View File

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

View File

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