From 7cd0e6154b0f5c7fdd1f958bf6839f95c6e46896 Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+unredundant@users.noreply.github.com> Date: Sat, 13 Aug 2022 14:54:16 -0700 Subject: [PATCH] fix: recursion (#293) --- .../io/bkbn/kompendium/core/KompendiumTest.kt | 5 +- .../resources/T0036__nullable_fields.json | 58 +++++++----- .../resources/T0042__simple_recursive.json | 94 +++++++++++++++++++ .../schema/handler/SimpleObjectHandler.kt | 21 +++-- 4 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 core/src/test/resources/T0042__simple_recursive.json diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 84b8a3531..026c22e76 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -40,6 +40,7 @@ import io.bkbn.kompendium.core.util.TestModules.returnsList import io.bkbn.kompendium.core.util.TestModules.rootRoute import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse import io.bkbn.kompendium.core.util.TestModules.simplePathParsing +import io.bkbn.kompendium.core.util.TestModules.simpleRecursive import io.bkbn.kompendium.core.util.TestModules.trailingSlash import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.json.schema.definition.TypeDefinition @@ -183,8 +184,8 @@ class KompendiumTest : DescribeSpec({ xit("Can override field name") { // TODO Assess strategies here } - xit("Can serialize a recursive type") { - // TODO openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() } + it("Can serialize a recursive type") { + openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() } } it("Nullable fields do not lead to doom") { openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() } diff --git a/core/src/test/resources/T0036__nullable_fields.json b/core/src/test/resources/T0036__nullable_fields.json index 82b949529..07d038445 100644 --- a/core/src/test/resources/T0036__nullable_fields.json +++ b/core/src/test/resources/T0036__nullable_fields.json @@ -62,30 +62,7 @@ "type": "null" }, { - "type": "object", - "properties": { - "isPrivate": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ] - }, - "otherThing": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string" - } - ] - } - }, - "required": [] + "$ref": "#/components/schemas/ProfileMetadataUpdateRequest" } ] }, @@ -112,6 +89,39 @@ } }, "required": [] + }, + "ProfileMetadataUpdateRequest": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "properties": { + "isPrivate": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + } + ] + }, + "otherThing": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + }, + "required": [] + } + ] } }, "securitySchemes": {} diff --git a/core/src/test/resources/T0042__simple_recursive.json b/core/src/test/resources/T0042__simple_recursive.json new file mode 100644 index 000000000..22d34fda3 --- /dev/null +++ b/core/src/test/resources/T0042__simple_recursive.json @@ -0,0 +1,94 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "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": { + "/": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ColumnSchema" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "ColumnSchema": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "mode": { + "enum": [ + "NULLABLE", + "REQUIRED", + "REPEATED" + ] + }, + "name": { + "type": "string" + }, + "subColumns": { + "items": { + "$ref": "#/components/schemas/ColumnSchema" + }, + "type": "array" + }, + "type": { + "type": "string" + } + }, + "required": [ + "description", + "mode", + "name", + "type" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt index d9d162c05..99edf33d9 100644 --- a/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt +++ b/json-schema/src/main/kotlin/io/bkbn/kompendium/json/schema/handler/SimpleObjectHandler.kt @@ -20,7 +20,9 @@ import kotlin.reflect.full.primaryConstructor object SimpleObjectHandler { fun handle(type: KType, clazz: KClass<*>, cache: MutableMap): JsonSchema { - // cache[type.getSimpleSlug()] = ReferenceDefinition("RECURSION_PLACEHOLDER") + + cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug()) + val typeMap = clazz.typeParameters.zip(type.arguments).toMap() val props = clazz.memberProperties.associate { prop -> val schema = when (prop.needsToInjectGenerics(typeMap)) { @@ -31,8 +33,7 @@ object SimpleObjectHandler { } } - // TODO This is kinda hacky 👀 And might break in certain edge cases? - val nullCheckSchema = when (prop.returnType.isMarkedNullable && schema !is OneOfDefinition) { + val nullCheckSchema = when (prop.returnType.isMarkedNullable && !schema.isNullable()) { true -> OneOfDefinition(NullableDefinition(), schema) false -> schema } @@ -77,7 +78,7 @@ object SimpleObjectHandler { } val constructedType = propClass.createType(types) return SchemaGenerator.fromTypeToSchema(constructedType, cache).let { - if (it is TypeDefinition && it.type == "object") { + if (it.isOrContainsObjectDef()) { cache[constructedType.getSimpleSlug()] = it ReferenceDefinition(prop.returnType.getReferenceSlug()) } else { @@ -93,7 +94,7 @@ object SimpleObjectHandler { ): JsonSchema { val type = typeMap[prop.returnType.classifier]?.type!! return SchemaGenerator.fromTypeToSchema(type, cache).let { - if (it is TypeDefinition && it.type == "object") { + if (it.isOrContainsObjectDef()) { cache[type.getSimpleSlug()] = it ReferenceDefinition(prop.returnType.getReferenceSlug()) } else { @@ -104,11 +105,19 @@ object SimpleObjectHandler { private fun handleProperty(prop: KProperty<*>, cache: MutableMap): JsonSchema = SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let { - if (it is TypeDefinition && it.type == "object") { + if (it.isOrContainsObjectDef()) { cache[prop.returnType.getSimpleSlug()] = it ReferenceDefinition(prop.returnType.getReferenceSlug()) } else { it } } + + private fun JsonSchema.isOrContainsObjectDef(): Boolean { + val isTypeDef = this is TypeDefinition && type == "object" + val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" } + return isTypeDef || isTypeDefOneOf + } + + private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any{ it is NullableDefinition } }