fix: nullable enum support (#234)

This commit is contained in:
Ryan Brink
2022-03-30 15:47:13 -06:00
committed by GitHub
parent cbfdacb596
commit ecd9415662
10 changed files with 142 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T>(val messy: String, val potato: T)
data class TestCreatedResponse(val id: Int, val c: String)

View File

@ -185,6 +185,12 @@ object TestResponseInfo {
responseInfo = simpleOkResponse()
)
val nullableEnumField = GetInfo<Unit, NullableEnum>(
summary = "Has a nullable enum field",
description = "should still work!",
responseInfo = simpleOkResponse()
)
val minMaxInt = GetInfo<Unit, MinMaxInt>(
summary = "Constrained int field",
description = "Cool stuff",

View File

@ -5,10 +5,27 @@ import kotlinx.serialization.Serializable
@Serializable
data class EnumSchema(
val `enum`: Set<String>,
val enum: Set<String>,
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
}
}

View File

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

View File

@ -40,8 +40,8 @@ private fun Application.mainModule() {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
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))
}
}
}