From 5fe9fffdee583ec3cdab5fa770c7e302b52794b4 Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+unredundant@users.noreply.github.com> Date: Sat, 5 Mar 2022 11:10:30 -0500 Subject: [PATCH] fix: free form annotation can be applied to top level type (#219) --- CHANGELOG.md | 4 ++ gradle.properties | 2 +- .../kompendium/annotations/FreeFormObject.kt | 2 +- .../kompendium/core/handler/ObjectHandler.kt | 32 +++++---- .../io/bkbn/kompendium/core/KompendiumTest.kt | 4 ++ .../bkbn/kompendium/core/util/TestModules.kt | 10 +++ .../src/test/resources/free_form_field.json | 70 +++++++++++++++++++ .../src/test/resources/free_form_object.json | 18 +---- .../kompendium/core/fixtures/TestModels.kt | 3 + .../core/fixtures/TestResponseInfo.kt | 8 ++- 10 files changed, 122 insertions(+), 31 deletions(-) create mode 100644 kompendium-core/src/test/resources/free_form_field.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a344d67f7..6fb42d109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ ## Released +## [2.3.1] - March 5th, 2022 +### Changed +- Can now apply `@FreeFormObject` to top level types + ## [2.3.0] - March 1st, 2022 ### Added - Brand new SwaggerUI support as a KTor plugin with WebJar under the hood and flexible configuration diff --git a/gradle.properties b/gradle.properties index e4cacedf8..a4b23948b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=2.3.0 +project.version=2.3.1 # Kotlin kotlin.code.style=official # Gradle diff --git a/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt b/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt index b7de9ed8e..d64b903c0 100644 --- a/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt +++ b/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt @@ -1,5 +1,5 @@ package io.bkbn.kompendium.annotations @Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.PROPERTY) +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class FreeFormObject diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt index 39fabea7d..112b9e50e 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt @@ -24,6 +24,7 @@ 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.jvm.javaField import org.slf4j.LoggerFactory @@ -44,19 +45,24 @@ object ObjectHandler : SchemaHandler { // 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 this should be some kind of empty schema at this point, then throw error if not updated eventually - cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug()) - val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap() - val fieldMap = clazz.generateFieldMap(typeMap, cache) - .plus(clazz.generateUndeclaredFieldMap(cache)) - .mapValues { (_, fieldSchema) -> - val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull() - postProcessSchema(fieldSchema, fieldSlug) - } - logger.debug("$slug contains $fieldMap") - val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz) - logger.debug("$slug schema: $schema") - cache[slug] = schema + // check if free form object + if (clazz.hasAnnotation()) { + cache[type.getSimpleSlug()] = FreeFormSchema() + } else { + // todo this should be some kind of empty schema at this point, then throw error if not updated eventually + cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug()) + val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap() + val fieldMap = clazz.generateFieldMap(typeMap, cache) + .plus(clazz.generateUndeclaredFieldMap(cache)) + .mapValues { (_, fieldSchema) -> + val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull() + postProcessSchema(fieldSchema, fieldSlug) + } + logger.debug("$slug contains $fieldMap") + val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz) + logger.debug("$slug schema: $schema") + cache[slug] = schema + } } } diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 0d3ebeded..e827ee7ec 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -18,6 +18,7 @@ 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.freeFormField import io.bkbn.kompendium.core.util.freeFormObject import io.bkbn.kompendium.core.util.genericPolymorphicResponse import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls @@ -302,6 +303,9 @@ class KompendiumTest : DescribeSpec({ } describe("Free Form") { 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() } } } diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt index 4071433e8..f77d4d6c4 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt @@ -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() { routing { route("/test/required_param") { diff --git a/kompendium-core/src/test/resources/free_form_field.json b/kompendium-core/src/test/resources/free_form_field.json new file mode 100644 index 000000000..8eae097a3 --- /dev/null +++ b/kompendium-core/src/test/resources/free_form_field.json @@ -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": [] +} diff --git a/kompendium-core/src/test/resources/free_form_object.json b/kompendium-core/src/test/resources/free_form_object.json index 8eae097a3..053137806 100644 --- a/kompendium-core/src/test/resources/free_form_object.json +++ b/kompendium-core/src/test/resources/free_form_object.json @@ -38,7 +38,8 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FreeFormData" + "additionalProperties": true, + "type": "object" } } } @@ -49,20 +50,7 @@ } }, "components": { - "schemas": { - "FreeFormData": { - "properties": { - "data": { - "additionalProperties": true, - "type": "object" - } - }, - "required": [ - "data" - ], - "type": "object" - } - }, + "schemas": {}, "securitySchemes": {} }, "security": [], diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt index 09f4ed8c0..5cee52a81 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt @@ -141,6 +141,9 @@ data class FreeFormData( val data: JsonElement ) +@FreeFormObject +object AnythingGoesMan + data class MinMaxFreeForm( @FreeFormObject @MinProperties(5) diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt index 2249346f4..e10b752dc 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt @@ -258,7 +258,13 @@ object TestResponseInfo { responseInfo = simpleOkResponse() ) - val freeFormObject = GetInfo( + val freeFormField = GetInfo( + summary = "required param", + description = "Cool stuff", + responseInfo = simpleOkResponse() + ) + + val freeFormObject = GetInfo( summary = "required param", description = "Cool stuff", responseInfo = simpleOkResponse()