fix: free form annotation can be applied to top level type (#219)

This commit is contained in:
Ryan Brink
2022-03-05 11:10:30 -05:00
committed by GitHub
parent 2364aaa754
commit 5fe9fffdee
10 changed files with 122 additions and 31 deletions

View File

@ -12,6 +12,10 @@
## Released ## Released
## [2.3.1] - March 5th, 2022
### Changed
- Can now apply `@FreeFormObject` to top level types
## [2.3.0] - March 1st, 2022 ## [2.3.0] - March 1st, 2022
### Added ### Added
- Brand new SwaggerUI support as a KTor plugin with WebJar under the hood and flexible configuration - Brand new SwaggerUI support as a KTor plugin with WebJar under the hood and flexible configuration

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=2.3.0 project.version=2.3.1
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle

View File

@ -1,5 +1,5 @@
package io.bkbn.kompendium.annotations package io.bkbn.kompendium.annotations
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class FreeFormObject annotation class FreeFormObject

View File

@ -24,6 +24,7 @@ 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.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -44,19 +45,24 @@ object ObjectHandler : SchemaHandler {
// Only analyze if component has not already been stored in the cache // Only analyze if component has not already been stored in the cache
if (!cache.containsKey(slug)) { if (!cache.containsKey(slug)) {
logger.debug("$slug was not found in cache, generating now") logger.debug("$slug was not found in cache, generating now")
// todo this should be some kind of empty schema at this point, then throw error if not updated eventually // check if free form object
cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug()) if (clazz.hasAnnotation<FreeFormObject>()) {
val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap() cache[type.getSimpleSlug()] = FreeFormSchema()
val fieldMap = clazz.generateFieldMap(typeMap, cache) } else {
.plus(clazz.generateUndeclaredFieldMap(cache)) // todo this should be some kind of empty schema at this point, then throw error if not updated eventually
.mapValues { (_, fieldSchema) -> cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug())
val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull() val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
postProcessSchema(fieldSchema, fieldSlug) val fieldMap = clazz.generateFieldMap(typeMap, cache)
} .plus(clazz.generateUndeclaredFieldMap(cache))
logger.debug("$slug contains $fieldMap") .mapValues { (_, fieldSchema) ->
val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz) val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull()
logger.debug("$slug schema: $schema") postProcessSchema(fieldSchema, fieldSlug)
cache[slug] = schema }
logger.debug("$slug contains $fieldMap")
val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)
logger.debug("$slug schema: $schema")
cache[slug] = schema
}
} }
} }

View File

@ -18,6 +18,7 @@ import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exclusiveMinMax import io.bkbn.kompendium.core.util.exclusiveMinMax
import io.bkbn.kompendium.core.util.formattedParam import io.bkbn.kompendium.core.util.formattedParam
import io.bkbn.kompendium.core.util.formattedType import io.bkbn.kompendium.core.util.formattedType
import io.bkbn.kompendium.core.util.freeFormField
import io.bkbn.kompendium.core.util.freeFormObject import io.bkbn.kompendium.core.util.freeFormObject
import io.bkbn.kompendium.core.util.genericPolymorphicResponse import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
@ -302,6 +303,9 @@ class KompendiumTest : DescribeSpec({
} }
describe("Free Form") { describe("Free Form") {
it("Can create a free-form field") { it("Can create a free-form field") {
openApiTestAllSerializers("free_form_field.json") { freeFormField() }
}
it("Can create a top-level free form object") {
openApiTestAllSerializers("free_form_object.json") { freeFormObject() } openApiTestAllSerializers("free_form_object.json") { freeFormObject() }
} }
} }

View File

@ -579,6 +579,16 @@ fun Application.multipleOfDouble() {
} }
} }
fun Application.freeFormField() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.freeFormField) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.freeFormObject() { fun Application.freeFormObject() {
routing { routing {
route("/test/required_param") { route("/test/required_param") {

View File

@ -0,0 +1,70 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FreeFormData"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"FreeFormData": {
"properties": {
"data": {
"additionalProperties": true,
"type": "object"
}
},
"required": [
"data"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -38,7 +38,8 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/FreeFormData" "additionalProperties": true,
"type": "object"
} }
} }
} }
@ -49,20 +50,7 @@
} }
}, },
"components": { "components": {
"schemas": { "schemas": {},
"FreeFormData": {
"properties": {
"data": {
"additionalProperties": true,
"type": "object"
}
},
"required": [
"data"
],
"type": "object"
}
},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -141,6 +141,9 @@ data class FreeFormData(
val data: JsonElement val data: JsonElement
) )
@FreeFormObject
object AnythingGoesMan
data class MinMaxFreeForm( data class MinMaxFreeForm(
@FreeFormObject @FreeFormObject
@MinProperties(5) @MinProperties(5)

View File

@ -258,7 +258,13 @@ object TestResponseInfo {
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val freeFormObject = GetInfo<Unit, FreeFormData>( val freeFormField = GetInfo<Unit, FreeFormData>(
summary = "required param",
description = "Cool stuff",
responseInfo = simpleOkResponse()
)
val freeFormObject = GetInfo<Unit, AnythingGoesMan>(
summary = "required param", summary = "required param",
description = "Cool stuff", description = "Cool stuff",
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()