fix: nullable enum support (#234)
This commit is contained in:
@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
## Released
|
## Released
|
||||||
|
|
||||||
|
## [2.3.2] - March 30th, 2022
|
||||||
|
### Changed
|
||||||
|
- Fixed bug where nullable enum fields caused runtime exceptions
|
||||||
|
|
||||||
## [2.3.1] - March 5th, 2022
|
## [2.3.1] - March 5th, 2022
|
||||||
### Changed
|
### Changed
|
||||||
- Can now apply `@FreeFormObject` to top level types
|
- Can now apply `@FreeFormObject` to top level types
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=2.3.1
|
project.version=2.3.2
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -41,6 +41,7 @@ import io.bkbn.kompendium.core.util.notarizedOptionsModule
|
|||||||
import io.bkbn.kompendium.core.util.notarizedPatchModule
|
import io.bkbn.kompendium.core.util.notarizedPatchModule
|
||||||
import io.bkbn.kompendium.core.util.notarizedPostModule
|
import io.bkbn.kompendium.core.util.notarizedPostModule
|
||||||
import io.bkbn.kompendium.core.util.notarizedPutModule
|
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.nullableField
|
||||||
import io.bkbn.kompendium.core.util.nullableNestedObject
|
import io.bkbn.kompendium.core.util.nullableNestedObject
|
||||||
import io.bkbn.kompendium.core.util.overrideFieldInfo
|
import io.bkbn.kompendium.core.util.overrideFieldInfo
|
||||||
@ -245,6 +246,9 @@ class KompendiumTest : DescribeSpec({
|
|||||||
it("Nullable fields do not lead to doom") {
|
it("Nullable fields do not lead to doom") {
|
||||||
openApiTestAllSerializers("nullable_fields.json") { nullableNestedObject() }
|
openApiTestAllSerializers("nullable_fields.json") { nullableNestedObject() }
|
||||||
}
|
}
|
||||||
|
it("Can have a nullable enum as a member field") {
|
||||||
|
openApiTestAllSerializers("nullable_enum_field.json") { nullableEnumField() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
describe("Constraints") {
|
describe("Constraints") {
|
||||||
it("Can set a minimum and maximum integer value") {
|
it("Can set a minimum and maximum integer value") {
|
||||||
|
@ -429,6 +429,16 @@ fun Application.nullableNestedObject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Application.nullableEnumField() {
|
||||||
|
routing {
|
||||||
|
route("/nullable/enum") {
|
||||||
|
notarizedGet(TestResponseInfo.nullableEnumField) {
|
||||||
|
call.respond(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Application.constrainedIntInfo() {
|
fun Application.constrainedIntInfo() {
|
||||||
routing {
|
routing {
|
||||||
route("/test/constrained_int") {
|
route("/test/constrained_int") {
|
||||||
|
73
kompendium-core/src/test/resources/nullable_enum_field.json
Normal file
73
kompendium-core/src/test/resources/nullable_enum_field.json
Normal 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": []
|
||||||
|
}
|
@ -63,6 +63,15 @@ data class TestRequest(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class TestResponse(val c: String)
|
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 TestGeneric<T>(val messy: String, val potato: T)
|
||||||
|
|
||||||
data class TestCreatedResponse(val id: Int, val c: String)
|
data class TestCreatedResponse(val id: Int, val c: String)
|
||||||
|
@ -185,6 +185,12 @@ object TestResponseInfo {
|
|||||||
responseInfo = simpleOkResponse()
|
responseInfo = simpleOkResponse()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val nullableEnumField = GetInfo<Unit, NullableEnum>(
|
||||||
|
summary = "Has a nullable enum field",
|
||||||
|
description = "should still work!",
|
||||||
|
responseInfo = simpleOkResponse()
|
||||||
|
)
|
||||||
|
|
||||||
val minMaxInt = GetInfo<Unit, MinMaxInt>(
|
val minMaxInt = GetInfo<Unit, MinMaxInt>(
|
||||||
summary = "Constrained int field",
|
summary = "Constrained int field",
|
||||||
description = "Cool stuff",
|
description = "Cool stuff",
|
||||||
|
@ -5,10 +5,27 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EnumSchema(
|
data class EnumSchema(
|
||||||
val `enum`: Set<String>,
|
val enum: Set<String>,
|
||||||
override val default: @Contextual Any? = null,
|
override val default: @Contextual Any? = null,
|
||||||
override val description: String? = null,
|
override val description: String? = null,
|
||||||
override val nullable: Boolean? = null
|
override val nullable: Boolean? = null
|
||||||
) : TypedSchema {
|
) : TypedSchema {
|
||||||
override val type: String = "string"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ private fun Application.mainModule() {
|
|||||||
swagger(pageTitle = "Swaggerlicious")
|
swagger(pageTitle = "Swaggerlicious")
|
||||||
// Kompendium infers the route path from the Ktor Route. This will show up as the root path `/`
|
// Kompendium infers the route path from the Ktor Route. This will show up as the root path `/`
|
||||||
notarizedGet(simpleGetExample) {
|
notarizedGet(simpleGetExample) {
|
||||||
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
|
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString(), d = null))
|
||||||
}
|
}
|
||||||
notarizedDelete(simpleDeleteRequest) {
|
notarizedDelete(simpleDeleteRequest) {
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(HttpStatusCode.NoContent)
|
||||||
@ -87,15 +87,15 @@ private fun Application.mainModule() {
|
|||||||
notarizedGet(simpleGetExampleWithParameters) {
|
notarizedGet(simpleGetExampleWithParameters) {
|
||||||
val a = call.parameters["a"] ?: error("Unable to read expected path parameter")
|
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")
|
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") {
|
route("/create") {
|
||||||
notarizedPost(simplePostRequest) {
|
notarizedPost(simplePostRequest) {
|
||||||
val request = call.receive<BasicRequest>()
|
val request = call.receive<BasicRequest>()
|
||||||
when (request.d) {
|
when (request.d) {
|
||||||
true -> call.respond(HttpStatusCode.OK, BasicResponse(c = "So it is true!"))
|
true -> call.respond(HttpStatusCode.OK, BasicResponse(c = "So it is true!", d = null))
|
||||||
false -> call.respond(HttpStatusCode.OK, BasicResponse(c = "Oh, I knew it!"))
|
false -> call.respond(HttpStatusCode.OK, BasicResponse(c = "Oh, I knew it!", d = BasicModels.BasicEnum.YES))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ object BasicPlaygroundToC {
|
|||||||
responseInfo = ResponseInfo(
|
responseInfo = ResponseInfo(
|
||||||
status = HttpStatusCode.OK,
|
status = HttpStatusCode.OK,
|
||||||
description = "This means everything went as expected!",
|
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")
|
tags = setOf("Simple")
|
||||||
)
|
)
|
||||||
@ -131,7 +131,7 @@ object BasicPlaygroundToC {
|
|||||||
responseInfo = ResponseInfo(
|
responseInfo = ResponseInfo(
|
||||||
status = HttpStatusCode.OK,
|
status = HttpStatusCode.OK,
|
||||||
description = "This means everything went as expected!",
|
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")
|
tags = setOf("Parameters")
|
||||||
)
|
)
|
||||||
@ -149,7 +149,7 @@ object BasicPlaygroundToC {
|
|||||||
responseInfo = ResponseInfo(
|
responseInfo = ResponseInfo(
|
||||||
status = HttpStatusCode.OK,
|
status = HttpStatusCode.OK,
|
||||||
description = "This means everything went as expected!",
|
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")
|
tags = setOf("Simple")
|
||||||
)
|
)
|
||||||
@ -169,8 +169,15 @@ object BasicPlaygroundToC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object BasicModels {
|
object BasicModels {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BasicResponse(val c: String)
|
enum class BasicEnum {
|
||||||
|
YES,
|
||||||
|
NO
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BasicResponse(val c: String, val d: BasicEnum? = null)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BasicParameters(
|
data class BasicParameters(
|
||||||
|
@ -40,8 +40,8 @@ private fun Application.mainModule() {
|
|||||||
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
|
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
|
||||||
val request = call.receive<BasicModels.BasicRequest>()
|
val request = call.receive<BasicModels.BasicRequest>()
|
||||||
when (request.d) {
|
when (request.d) {
|
||||||
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
|
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!"))
|
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!", null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user