fix: nullability breaks object comparison (#202)

This commit is contained in:
Ryan Brink
2022-02-19 18:39:57 -05:00
committed by GitHub
parent c50f7f7ccc
commit 64c352064e
11 changed files with 194 additions and 6 deletions

View File

@ -5,7 +5,6 @@
### Added
### Changed
- Fixed sealed typed collections schema generation
### Remove
@ -13,6 +12,11 @@
## 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
### Added
- Ability to override serializer via custom route

View File

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

View File

@ -51,7 +51,7 @@ object ObjectHandler : SchemaHandler {
.plus(clazz.generateUndeclaredFieldMap(cache))
.mapValues { (_, fieldSchema) ->
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")
val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)

View File

@ -24,9 +24,15 @@ interface SchemaHandler {
}
}
fun postProcessSchema(schema: ComponentSchema, slug: String): ComponentSchema = when (schema) {
is ObjectSchema -> ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
is EnumSchema -> ReferencedSchema(COMPONENT_SLUG.plus("/").plus(slug))
fun postProcessSchema(schema: ComponentSchema, slug: String?): ComponentSchema = when (schema) {
is ObjectSchema -> {
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
}
}

View File

@ -39,6 +39,7 @@ 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.nullableField
import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.overrideFieldInfo
import io.bkbn.kompendium.core.util.pathParsingTestModule
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
@ -235,6 +236,9 @@ class KompendiumTest : DescribeSpec({
it("Can serialize a recursive type") {
openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() }
}
it("Nullable fields do not lead to doom") {
openApiTestAllSerializers("nullable_fields.json") { nullableNestedObject() }
}
}
describe("Constraints") {
it("Can set a minimum and maximum integer value") {

View File

@ -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.minMaxString
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.requiredParam
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() {
routing {
route("/test/constrained_int") {

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

View File

@ -235,3 +235,17 @@ data class ColumnSchema(
val mode: ColumnMode,
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?
)

View File

@ -175,6 +175,15 @@ object TestResponseInfo {
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>(
summary = "Constrained int field",
description = "Cool stuff",

View File

@ -13,4 +13,24 @@ data class ObjectSchema(
val required: List<String>? = null
) : TypedSchema {
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
}
}

View File

@ -189,3 +189,4 @@ object BasicModels {
val d: Boolean
)
}