fix: nullability breaks object comparison (#202)
This commit is contained in:
@ -5,7 +5,6 @@
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Fixed sealed typed collections schema generation
|
|
||||||
|
|
||||||
### Remove
|
### Remove
|
||||||
|
|
||||||
@ -13,6 +12,11 @@
|
|||||||
|
|
||||||
## Released
|
## Released
|
||||||
|
|
||||||
|
## [2.1.1] - February 19th, 2022
|
||||||
|
### Changed
|
||||||
|
- Fixed sealed typed collections schema generation
|
||||||
|
- Nullability no longer breaks object schema comparison
|
||||||
|
|
||||||
## [2.1.0] - February 18th, 2022
|
## [2.1.0] - February 18th, 2022
|
||||||
### Added
|
### Added
|
||||||
- Ability to override serializer via custom route
|
- Ability to override serializer via custom route
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=2.1.0
|
project.version=2.1.1
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -51,7 +51,7 @@ object ObjectHandler : SchemaHandler {
|
|||||||
.plus(clazz.generateUndeclaredFieldMap(cache))
|
.plus(clazz.generateUndeclaredFieldMap(cache))
|
||||||
.mapValues { (_, fieldSchema) ->
|
.mapValues { (_, fieldSchema) ->
|
||||||
val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull()
|
val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull()
|
||||||
postProcessSchema(fieldSchema, fieldSlug ?: "Fine if blank, will be ignored")
|
postProcessSchema(fieldSchema, fieldSlug)
|
||||||
}
|
}
|
||||||
logger.debug("$slug contains $fieldMap")
|
logger.debug("$slug contains $fieldMap")
|
||||||
val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)
|
val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)
|
||||||
|
@ -24,9 +24,15 @@ interface SchemaHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postProcessSchema(schema: ComponentSchema, slug: String): ComponentSchema = when (schema) {
|
fun postProcessSchema(schema: ComponentSchema, slug: String?): ComponentSchema = when (schema) {
|
||||||
is ObjectSchema -> ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
|
is ObjectSchema -> {
|
||||||
is EnumSchema -> ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
|
require(slug != null) { "Slug cannot be null for an object schema! $schema" }
|
||||||
|
ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
|
||||||
|
}
|
||||||
|
is EnumSchema -> {
|
||||||
|
require(slug != null) { "Slug cannot be null for an enum schema! $schema" }
|
||||||
|
ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
|
||||||
|
}
|
||||||
else -> schema
|
else -> schema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ 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.nullableField
|
import io.bkbn.kompendium.core.util.nullableField
|
||||||
|
import io.bkbn.kompendium.core.util.nullableNestedObject
|
||||||
import io.bkbn.kompendium.core.util.overrideFieldInfo
|
import io.bkbn.kompendium.core.util.overrideFieldInfo
|
||||||
import io.bkbn.kompendium.core.util.pathParsingTestModule
|
import io.bkbn.kompendium.core.util.pathParsingTestModule
|
||||||
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
|
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
|
||||||
@ -235,6 +236,9 @@ class KompendiumTest : DescribeSpec({
|
|||||||
it("Can serialize a recursive type") {
|
it("Can serialize a recursive type") {
|
||||||
openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() }
|
openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() }
|
||||||
}
|
}
|
||||||
|
it("Nullable fields do not lead to doom") {
|
||||||
|
openApiTestAllSerializers("nullable_fields.json") { nullableNestedObject() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
describe("Constraints") {
|
describe("Constraints") {
|
||||||
it("Can set a minimum and maximum integer value") {
|
it("Can set a minimum and maximum integer value") {
|
||||||
|
@ -24,6 +24,7 @@ import io.bkbn.kompendium.core.fixtures.TestResponseInfo.defaultParam
|
|||||||
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.formattedParam
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.formattedParam
|
||||||
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.minMaxString
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.minMaxString
|
||||||
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.nullableField
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.nullableField
|
||||||
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.nullableNested
|
||||||
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.regexString
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.regexString
|
||||||
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.requiredParam
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.requiredParam
|
||||||
import io.bkbn.kompendium.core.metadata.RequestInfo
|
import io.bkbn.kompendium.core.metadata.RequestInfo
|
||||||
@ -415,6 +416,16 @@ fun Application.simpleRecursive() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Application.nullableNestedObject() {
|
||||||
|
routing {
|
||||||
|
route("/nullable/nested") {
|
||||||
|
notarizedPost(nullableNested) {
|
||||||
|
call.respond(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Application.constrainedIntInfo() {
|
fun Application.constrainedIntInfo() {
|
||||||
routing {
|
routing {
|
||||||
route("/test/constrained_int") {
|
route("/test/constrained_int") {
|
||||||
|
119
kompendium-core/src/test/resources/nullable_fields.json
Normal file
119
kompendium-core/src/test/resources/nullable_fields.json
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"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/nested": {
|
||||||
|
"post": {
|
||||||
|
"tags": [],
|
||||||
|
"summary": "Has a bunch of nullable fields",
|
||||||
|
"description": "Should still work!",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Cool",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProfileUpdateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A successful endeavor",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ProfileUpdateRequest": {
|
||||||
|
"properties": {
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/components/schemas/ProfileMetadataUpdateRequest"
|
||||||
|
},
|
||||||
|
"mood": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"viewCount": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mood",
|
||||||
|
"viewCount",
|
||||||
|
"metadata"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ProfileMetadataUpdateRequest": {
|
||||||
|
"properties": {
|
||||||
|
"isPrivate": {
|
||||||
|
"type": "boolean",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"otherThing": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isPrivate",
|
||||||
|
"otherThing"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"TestResponse": {
|
||||||
|
"properties": {
|
||||||
|
"c": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -235,3 +235,17 @@ data class ColumnSchema(
|
|||||||
val mode: ColumnMode,
|
val mode: ColumnMode,
|
||||||
val subColumns: List<ColumnSchema> = emptyList()
|
val subColumns: List<ColumnSchema> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public data class ProfileUpdateRequest(
|
||||||
|
public val mood: String?,
|
||||||
|
public val viewCount: Long?,
|
||||||
|
public val metadata: ProfileMetadataUpdateRequest?
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public data class ProfileMetadataUpdateRequest(
|
||||||
|
public val isPrivate: Boolean?,
|
||||||
|
public val otherThing: String?
|
||||||
|
)
|
||||||
|
@ -175,6 +175,15 @@ object TestResponseInfo {
|
|||||||
responseInfo = simpleOkResponse()
|
responseInfo = simpleOkResponse()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val nullableNested = PostInfo<Unit, ProfileUpdateRequest, TestResponse>(
|
||||||
|
summary = "Has a bunch of nullable fields",
|
||||||
|
description = "Should still work!",
|
||||||
|
requestInfo = RequestInfo(
|
||||||
|
description = "Cool"
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
@ -13,4 +13,24 @@ data class ObjectSchema(
|
|||||||
val required: List<String>? = null
|
val required: List<String>? = null
|
||||||
) : TypedSchema {
|
) : TypedSchema {
|
||||||
override val type = "object"
|
override val type = "object"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is ObjectSchema) return false
|
||||||
|
if (properties != other.properties) return false
|
||||||
|
if (description != other.description) return false
|
||||||
|
// TODO Going to need some way to differentiate nullable vs non-nullable reference schemas 😬
|
||||||
|
// if (nullable != other.nullable) return false
|
||||||
|
if (required != other.required) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = properties.hashCode()
|
||||||
|
result = 31 * result + (default?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (description?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (nullable?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (required?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + type.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,3 +189,4 @@ object BasicModels {
|
|||||||
val d: Boolean
|
val d: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user