feat: create schema reference for enum types (#368)
This commit is contained in:
@ -6,6 +6,9 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Schemas for types in nullable properties are no longer nullable themselves
|
||||
- Enums are now generated as references, which makes it possible to generate types for them
|
||||
|
||||
### Remove
|
||||
|
||||
---
|
||||
|
@ -12,6 +12,8 @@ import io.bkbn.kompendium.core.metadata.PutInfo
|
||||
import io.bkbn.kompendium.core.metadata.ResponseInfo
|
||||
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
|
||||
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
||||
@ -108,7 +110,10 @@ object Helpers {
|
||||
Unit::class -> null
|
||||
else -> mapOf(
|
||||
"application/json" to MediaType(
|
||||
schema = ReferenceDefinition(this.getReferenceSlug()),
|
||||
schema = if (this.isMarkedNullable) OneOfDefinition(
|
||||
NullableDefinition(),
|
||||
ReferenceDefinition(this.getReferenceSlug())
|
||||
) else ReferenceDefinition(this.getReferenceSlug()),
|
||||
examples = examples
|
||||
)
|
||||
)
|
||||
|
@ -49,6 +49,7 @@ import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse
|
||||
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing
|
||||
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive
|
||||
import io.bkbn.kompendium.core.util.TestModules.singleException
|
||||
import io.bkbn.kompendium.core.util.TestModules.topLevelNullable
|
||||
import io.bkbn.kompendium.core.util.TestModules.trailingSlash
|
||||
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse
|
||||
import io.bkbn.kompendium.core.util.TestModules.withOperationId
|
||||
@ -243,6 +244,9 @@ class KompendiumTest : DescribeSpec({
|
||||
it("Can handle nested type names") {
|
||||
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
|
||||
}
|
||||
it("Can handle top level nullable types") {
|
||||
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
|
||||
}
|
||||
}
|
||||
describe("Error Handling") {
|
||||
it("Throws a clear exception when an unidentified type is encountered") {
|
||||
|
@ -24,7 +24,7 @@ import io.bkbn.kompendium.core.fixtures.TestRequest
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
||||
import io.bkbn.kompendium.core.fixtures.TransientObject
|
||||
import io.bkbn.kompendium.core.fixtures.UnbakcedObject
|
||||
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.bkbn.kompendium.core.metadata.HeadInfo
|
||||
@ -568,7 +568,7 @@ object TestModules {
|
||||
|
||||
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
||||
|
||||
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbakcedObject>()
|
||||
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
|
||||
|
||||
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
||||
|
||||
@ -613,6 +613,8 @@ object TestModules {
|
||||
|
||||
fun Routing.nestedTypeName() = basicGetGenerator<Nested.Response>()
|
||||
|
||||
fun Routing.topLevelNullable() = basicGetGenerator<TestResponse?>()
|
||||
|
||||
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
|
||||
|
||||
fun Routing.defaultAuthConfig() {
|
||||
|
@ -124,15 +124,19 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enumeration": {
|
||||
"enum": [
|
||||
"ONE",
|
||||
"TWO"
|
||||
]
|
||||
"$ref": "#/components/schemas/SimpleEnum"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enumeration"
|
||||
]
|
||||
},
|
||||
"SimpleEnum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ONE",
|
||||
"TWO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
|
@ -91,37 +91,30 @@
|
||||
"required": []
|
||||
},
|
||||
"ProfileMetadataUpdateRequest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isPrivate": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isPrivate": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
"otherThing": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
]
|
||||
},
|
||||
"otherThing": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
|
@ -62,16 +62,21 @@
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"YES",
|
||||
"NO"
|
||||
]
|
||||
"$ref": "#/components/schemas/TestEnum"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"TestEnum":
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"YES",
|
||||
"NO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
|
@ -57,10 +57,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enumeration": {
|
||||
"enum": [
|
||||
"ONE",
|
||||
"TWO"
|
||||
]
|
||||
"$ref": "#/components/schemas/SimpleEnum"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -120,6 +117,13 @@
|
||||
"required": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
"SimpleEnum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ONE",
|
||||
"TWO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
|
@ -53,6 +53,14 @@
|
||||
"webhooks": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ColumnMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NULLABLE",
|
||||
"REQUIRED",
|
||||
"REPEATED"
|
||||
]
|
||||
},
|
||||
"ColumnSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -60,11 +68,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"NULLABLE",
|
||||
"REQUIRED",
|
||||
"REPEATED"
|
||||
]
|
||||
"$ref": "#/components/schemas/ColumnMode"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
@ -39,7 +39,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnbakcedObject"
|
||||
"$ref": "#/components/schemas/UnbackedObject"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@
|
||||
"webhooks": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"UnbakcedObject": {
|
||||
"UnbackedObject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backed": {
|
||||
|
79
core/src/test/resources/T0051__top_level_nullable.json
Normal file
79
core/src/test/resources/T0051__top_level_nullable.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
|
||||
"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": {
|
||||
"/": {
|
||||
"get": {
|
||||
"tags": [],
|
||||
"summary": "Great Summary!",
|
||||
"description": "testing more",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A Successful Endeavor",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TestResponse"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": []
|
||||
}
|
||||
},
|
||||
"webhooks": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"TestResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"security": [],
|
||||
"tags": []
|
||||
}
|
@ -163,7 +163,7 @@ data class TransientObject(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnbakcedObject(
|
||||
data class UnbackedObject(
|
||||
val backed: String
|
||||
) {
|
||||
val unbacked: String get() = "unbacked"
|
||||
@ -176,3 +176,14 @@ data class SerialNameObject(
|
||||
@SerialName("snake_case_name")
|
||||
val camelCaseName: String
|
||||
)
|
||||
|
||||
enum class Color {
|
||||
RED,
|
||||
GREEN,
|
||||
BLUE
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ObjectWithEnum(
|
||||
val color: Color
|
||||
)
|
||||
|
@ -48,7 +48,7 @@ object SchemaGenerator {
|
||||
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
|
||||
UUID::class -> checkForNull(type, TypeDefinition.UUID)
|
||||
else -> when {
|
||||
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz)
|
||||
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
|
||||
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator)
|
||||
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator)
|
||||
else -> {
|
||||
|
@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class EnumDefinition(
|
||||
val type: String,
|
||||
val enum: Set<String>
|
||||
) : JsonSchema
|
||||
|
@ -2,18 +2,17 @@ package io.bkbn.kompendium.json.schema.handler
|
||||
|
||||
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
|
||||
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
|
||||
object EnumHandler {
|
||||
fun handle(type: KType, clazz: KClass<*>): JsonSchema {
|
||||
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
|
||||
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
|
||||
|
||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||
val definition = EnumDefinition(enum = options)
|
||||
return when (type.isMarkedNullable) {
|
||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
||||
false -> definition
|
||||
}
|
||||
return EnumDefinition(type = "string", enum = options)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema.handler
|
||||
|
||||
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||
@ -71,16 +72,11 @@ object SimpleObjectHandler {
|
||||
.map { schemaConfigurator.serializableName(it) }
|
||||
.toSet()
|
||||
|
||||
val definition = TypeDefinition(
|
||||
return TypeDefinition(
|
||||
type = "object",
|
||||
properties = props,
|
||||
required = required
|
||||
)
|
||||
|
||||
return when (type.isMarkedNullable) {
|
||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
||||
false -> definition
|
||||
}
|
||||
}
|
||||
|
||||
private fun KProperty<*>.needsToInjectGenerics(
|
||||
@ -103,7 +99,7 @@ object SimpleObjectHandler {
|
||||
}
|
||||
val constructedType = propClass.createType(types)
|
||||
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let {
|
||||
if (it.isOrContainsObjectDef()) {
|
||||
if (it.isOrContainsObjectOrEnumDef()) {
|
||||
cache[constructedType.getSimpleSlug()] = it
|
||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
||||
} else {
|
||||
@ -121,7 +117,7 @@ object SimpleObjectHandler {
|
||||
val type = typeMap[prop.returnType.classifier]?.type
|
||||
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
|
||||
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let {
|
||||
if (it.isOrContainsObjectDef()) {
|
||||
if (it.isOrContainsObjectOrEnumDef()) {
|
||||
cache[type.getSimpleSlug()] = it
|
||||
ReferenceDefinition(type.getReferenceSlug())
|
||||
} else {
|
||||
@ -136,7 +132,7 @@ object SimpleObjectHandler {
|
||||
schemaConfigurator: SchemaConfigurator
|
||||
): JsonSchema =
|
||||
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
|
||||
if (it.isOrContainsObjectDef()) {
|
||||
if (it.isOrContainsObjectOrEnumDef()) {
|
||||
cache[prop.returnType.getSimpleSlug()] = it
|
||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
||||
} else {
|
||||
@ -144,10 +140,12 @@ object SimpleObjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonSchema.isOrContainsObjectDef(): Boolean {
|
||||
private fun JsonSchema.isOrContainsObjectOrEnumDef(): Boolean {
|
||||
val isTypeDef = this is TypeDefinition && type == "object"
|
||||
val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" }
|
||||
return isTypeDef || isTypeDefOneOf
|
||||
val isEnumDef = this is EnumDefinition
|
||||
val isEnumDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is EnumDefinition }
|
||||
return isTypeDef || isTypeDefOneOf || isEnumDef || isEnumDefOneOf
|
||||
}
|
||||
|
||||
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }
|
||||
|
@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema
|
||||
|
||||
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
||||
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
|
||||
import io.bkbn.kompendium.core.fixtures.ObjectWithEnum
|
||||
import io.bkbn.kompendium.core.fixtures.SerialNameObject
|
||||
import io.bkbn.kompendium.core.fixtures.SimpleEnum
|
||||
import io.bkbn.kompendium.core.fixtures.SlammaJamma
|
||||
@ -9,7 +10,7 @@ import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
||||
import io.bkbn.kompendium.core.fixtures.TransientObject
|
||||
import io.bkbn.kompendium.core.fixtures.UnbakcedObject
|
||||
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||
import io.kotest.assertions.json.shouldEqualJson
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
@ -40,6 +41,7 @@ class SchemaGeneratorTest : DescribeSpec({
|
||||
jsonSchemaTest<ComplexRequest>("T0005__complex_object.json")
|
||||
}
|
||||
it("Can generate the schema for a nullable object") {
|
||||
// Same schema as a non-nullable type, since the nullability will be handled on the property
|
||||
jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json")
|
||||
}
|
||||
it("Can generate the schema for a polymorphic object") {
|
||||
@ -52,7 +54,7 @@ class SchemaGeneratorTest : DescribeSpec({
|
||||
jsonSchemaTest<TransientObject>("T0018__transient_object.json")
|
||||
}
|
||||
it("Can generate the schema for object with unbacked property") {
|
||||
jsonSchemaTest<UnbakcedObject>("T0019__unbacked_object.json")
|
||||
jsonSchemaTest<UnbackedObject>("T0019__unbacked_object.json")
|
||||
}
|
||||
it("Can generate the schema for object with SerialName annotation") {
|
||||
jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json")
|
||||
@ -63,8 +65,12 @@ class SchemaGeneratorTest : DescribeSpec({
|
||||
jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json")
|
||||
}
|
||||
it("Can generate the schema for a nullable enum") {
|
||||
// Same schema as a non-nullable enum, since the nullability will be handled on the property
|
||||
jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json")
|
||||
}
|
||||
it("Can generate the schema for an object with an enum property") {
|
||||
jsonSchemaTest<ObjectWithEnum>("T0021__object_with_enum.json")
|
||||
}
|
||||
}
|
||||
describe("Arrays") {
|
||||
it("Can generate the schema for an array of scalars") {
|
||||
|
@ -1,23 +1,16 @@
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "string"
|
||||
},
|
||||
"b": {
|
||||
"type": "number",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
"b": {
|
||||
"type": "number",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
{
|
||||
"enum": [ "ONE", "TWO" ]
|
||||
"enum": [ "ONE", "TWO" ],
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -1,13 +1,4 @@
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"ONE",
|
||||
"TWO"
|
||||
]
|
||||
}
|
||||
]
|
||||
"enum": [ "ONE", "TWO" ],
|
||||
"type": "string"
|
||||
}
|
||||
|
11
json-schema/src/test/resources/T0021__object_with_enum.json
Normal file
11
json-schema/src/test/resources/T0021__object_with_enum.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"$ref": "#/components/schemas/Color"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"color"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user