From ecd941566267a664c1d277a58863972505017f8e Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+unredundant@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:47:13 -0600 Subject: [PATCH] fix: nullable enum support (#234) --- CHANGELOG.md | 4 + gradle.properties | 2 +- .../io/bkbn/kompendium/core/KompendiumTest.kt | 4 + .../bkbn/kompendium/core/util/TestModules.kt | 10 +++ .../test/resources/nullable_enum_field.json | 73 +++++++++++++++++++ .../kompendium/core/fixtures/TestModels.kt | 9 +++ .../core/fixtures/TestResponseInfo.kt | 6 ++ .../bkbn/kompendium/oas/schema/EnumSchema.kt | 19 ++++- .../kompendium/playground/BasicPlayground.kt | 23 ++++-- .../playground/GsonSerializationPlayground.kt | 4 +- 10 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 kompendium-core/src/test/resources/nullable_enum_field.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb42d109..858b1b3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ ## Released +## [2.3.2] - March 30th, 2022 +### Changed +- Fixed bug where nullable enum fields caused runtime exceptions + ## [2.3.1] - March 5th, 2022 ### Changed - Can now apply `@FreeFormObject` to top level types diff --git a/gradle.properties b/gradle.properties index 546f216a6..e40a04655 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=2.3.1 +project.version=2.3.2 # Kotlin kotlin.code.style=official # Gradle 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 e827ee7ec..4b36a1e2f 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 @@ -41,6 +41,7 @@ import io.bkbn.kompendium.core.util.notarizedOptionsModule import io.bkbn.kompendium.core.util.notarizedPatchModule import io.bkbn.kompendium.core.util.notarizedPostModule import io.bkbn.kompendium.core.util.notarizedPutModule +import io.bkbn.kompendium.core.util.nullableEnumField import io.bkbn.kompendium.core.util.nullableField import io.bkbn.kompendium.core.util.nullableNestedObject import io.bkbn.kompendium.core.util.overrideFieldInfo @@ -245,6 +246,9 @@ class KompendiumTest : DescribeSpec({ it("Nullable fields do not lead to doom") { openApiTestAllSerializers("nullable_fields.json") { nullableNestedObject() } } + it("Can have a nullable enum as a member field") { + openApiTestAllSerializers("nullable_enum_field.json") { nullableEnumField() } + } } describe("Constraints") { it("Can set a minimum and maximum integer value") { 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 f77d4d6c4..c8f61a55b 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 @@ -429,6 +429,16 @@ fun Application.nullableNestedObject() { } } +fun Application.nullableEnumField() { + routing { + route("/nullable/enum") { + notarizedGet(TestResponseInfo.nullableEnumField) { + call.respond(HttpStatusCode.OK) + } + } + } +} + fun Application.constrainedIntInfo() { routing { route("/test/constrained_int") { diff --git a/kompendium-core/src/test/resources/nullable_enum_field.json b/kompendium-core/src/test/resources/nullable_enum_field.json new file mode 100644 index 000000000..a100ca747 --- /dev/null +++ b/kompendium-core/src/test/resources/nullable_enum_field.json @@ -0,0 +1,73 @@ +{ + "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": { + "/nullable/enum": { + "get": { + "tags": [], + "summary": "Has a nullable enum field", + "description": "should still work!", + "parameters": [], + "responses": { + "200": { + "description": "A successful endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableEnum" + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "schemas": { + "NullableEnum": { + "properties": { + "a": { + "$ref": "#/components/schemas/TestEnum" + } + }, + "type": "object" + }, + "TestEnum": { + "enum": [ + "YES", + "NO" + ], + "type": "string" + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} 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 5cee52a81..f237f293f 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 @@ -63,6 +63,15 @@ data class TestRequest( @Serializable data class TestResponse(val c: String) +@Serializable +enum class TestEnum { + YES, + NO +} + +@Serializable +data class NullableEnum(val a: TestEnum? = null) + data class TestGeneric(val messy: String, val potato: T) data class TestCreatedResponse(val id: Int, val c: String) 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 e10b752dc..076071523 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 @@ -185,6 +185,12 @@ object TestResponseInfo { responseInfo = simpleOkResponse() ) + val nullableEnumField = GetInfo( + summary = "Has a nullable enum field", + description = "should still work!", + responseInfo = simpleOkResponse() + ) + val minMaxInt = GetInfo( summary = "Constrained int field", description = "Cool stuff", diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/EnumSchema.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/EnumSchema.kt index abe701fff..7750af1b8 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/EnumSchema.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/EnumSchema.kt @@ -5,10 +5,27 @@ import kotlinx.serialization.Serializable @Serializable data class EnumSchema( - val `enum`: Set, + val enum: Set, override val default: @Contextual Any? = null, override val description: String? = null, override val nullable: Boolean? = null ) : TypedSchema { override val type: String = "string" + + override fun equals(other: Any?): Boolean { + if (other !is EnumSchema) return false + if (enum != other.enum) return false + // TODO Going to need some way to differentiate nullable vs non-nullable reference schemas 😬 + // if (nullable != other.nullable) return false + return true + } + + override fun hashCode(): Int { + var result = enum.hashCode() + result = 31 * result + (default?.hashCode() ?: 0) + result = 31 * result + (description?.hashCode() ?: 0) + result = 31 * result + (nullable?.hashCode() ?: 0) + result = 31 * result + type.hashCode() + return result + } } diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt index ccd092b4a..12b807409 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt @@ -77,7 +77,7 @@ private fun Application.mainModule() { swagger(pageTitle = "Swaggerlicious") // Kompendium infers the route path from the Ktor Route. This will show up as the root path `/` notarizedGet(simpleGetExample) { - call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString())) + call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString(), d = null)) } notarizedDelete(simpleDeleteRequest) { call.respond(HttpStatusCode.NoContent) @@ -87,15 +87,15 @@ private fun Application.mainModule() { notarizedGet(simpleGetExampleWithParameters) { val a = call.parameters["a"] ?: error("Unable to read expected path parameter") val b = call.request.queryParameters["b"]?.toInt() ?: error("Unable to read expected query parameter") - call.respond(HttpStatusCode.OK, BasicResponse(c = "$a: $b")) + call.respond(HttpStatusCode.OK, BasicResponse(c = "$a: $b", d = BasicModels.BasicEnum.NO)) } } route("/create") { notarizedPost(simplePostRequest) { val request = call.receive() when (request.d) { - true -> call.respond(HttpStatusCode.OK, BasicResponse(c = "So it is true!")) - false -> call.respond(HttpStatusCode.OK, BasicResponse(c = "Oh, I knew it!")) + true -> call.respond(HttpStatusCode.OK, BasicResponse(c = "So it is true!", d = null)) + false -> call.respond(HttpStatusCode.OK, BasicResponse(c = "Oh, I knew it!", d = BasicModels.BasicEnum.YES)) } } } @@ -115,7 +115,7 @@ object BasicPlaygroundToC { responseInfo = ResponseInfo( status = HttpStatusCode.OK, description = "This means everything went as expected!", - examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4")) + examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4", BasicModels.BasicEnum.NO)) ), tags = setOf("Simple") ) @@ -131,7 +131,7 @@ object BasicPlaygroundToC { responseInfo = ResponseInfo( status = HttpStatusCode.OK, description = "This means everything went as expected!", - examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4")) + examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4", BasicModels.BasicEnum.YES)) ), tags = setOf("Parameters") ) @@ -149,7 +149,7 @@ object BasicPlaygroundToC { responseInfo = ResponseInfo( status = HttpStatusCode.OK, description = "This means everything went as expected!", - examples = mapOf("demo" to BasicResponse(c = "So it is true!")) + examples = mapOf("demo" to BasicResponse(c = "So it is true!", null)) ), tags = setOf("Simple") ) @@ -169,8 +169,15 @@ object BasicPlaygroundToC { } object BasicModels { + @Serializable - data class BasicResponse(val c: String) + enum class BasicEnum { + YES, + NO + } + + @Serializable + data class BasicResponse(val c: String, val d: BasicEnum? = null) @Serializable data class BasicParameters( diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt index 04ee780a3..89dc39f2b 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt @@ -40,8 +40,8 @@ private fun Application.mainModule() { notarizedPost(BasicPlaygroundToC.simplePostRequest) { val request = call.receive() when (request.d) { - true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!")) - false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!")) + true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!", null)) + false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!", null)) } } }