fix: support recursive types (#174)

This commit is contained in:
Ryan Brink
2022-02-04 12:34:21 -05:00
committed by GitHub
parent a2be2c9e93
commit d54b8a730f
77 changed files with 487 additions and 8 deletions

View File

@ -12,6 +12,10 @@
## Released ## Released
## [2.0.2] - February 4th, 2022
### Added
- `@Referenced` annotation enabling support for recursive models
## [2.0.1] - January 23rd, 2022 ## [2.0.1] - January 23rd, 2022
### Change ### Change

View File

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

View File

@ -0,0 +1,11 @@
package io.bkbn.kompendium.annotations
/**
* This instructs Kompendium to store the class as a referenced component.
* This is mandatory for any data models that have recursive children.
* If you do not annotate a recursive class with [Referenced], you will
* get a stack overflow error when you try to launch your API
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Referenced

View File

@ -82,6 +82,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": { "securitySchemes": {
"basic": { "basic": {
"type": "http", "type": "http",

View File

@ -82,6 +82,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": { "securitySchemes": {
"jwt": { "jwt": {
"bearerFormat": "JWT", "bearerFormat": "JWT",

View File

@ -82,6 +82,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": { "securitySchemes": {
"oauth": { "oauth": {
"flows": { "flows": {

View File

@ -1,5 +1,16 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.util.Helpers.COMPONENT_SLUG
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ArraySchema
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.ReferencedSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema
import io.ktor.application.feature import io.ktor.application.feature
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.application import io.ktor.routing.application
@ -35,5 +46,32 @@ object KompendiumPreFlight {
feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache) feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache) feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache) feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
feature.updateReferences()
}
private fun Kompendium.updateReferences() {
val references = config.cache.values
.asSequence()
.map { flattenSchema(it) }
.flatten()
.filterIsInstance<ReferencedSchema>()
.map { it.`$ref` }
.map { it.replace(COMPONENT_SLUG.plus("/"), "") }
.toList()
references.forEach { ref ->
config.spec.components.schemas[ref] = config.cache[ref] ?: error("$ref does not exist in cache 😱")
}
}
private fun flattenSchema(schema: ComponentSchema): List<ComponentSchema> = when (schema) {
is AnyOfSchema -> schema.anyOf.map { flattenSchema(it) }.flatten()
is ReferencedSchema -> listOf(schema)
is ArraySchema -> flattenSchema(schema.items)
is DictionarySchema -> flattenSchema(schema.additionalProperties)
is EnumSchema -> listOf(schema)
is FormattedSchema -> listOf(schema)
is FreeFormSchema -> listOf(schema)
is ObjectSchema -> schema.properties.values.map { flattenSchema(it) }.flatten()
is SimpleSchema -> listOf(schema)
} }
} }

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Field import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.FreeFormObject import io.bkbn.kompendium.annotations.FreeFormObject
import io.bkbn.kompendium.annotations.Referenced
import io.bkbn.kompendium.annotations.UndeclaredField import io.bkbn.kompendium.annotations.UndeclaredField
import io.bkbn.kompendium.annotations.constraint.Format import io.bkbn.kompendium.annotations.constraint.Format
import io.bkbn.kompendium.annotations.constraint.MaxItems import io.bkbn.kompendium.annotations.constraint.MaxItems
@ -18,6 +19,7 @@ import io.bkbn.kompendium.annotations.constraint.UniqueItems
import io.bkbn.kompendium.core.metadata.SchemaMap import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.core.metadata.TypeMap import io.bkbn.kompendium.core.metadata.TypeMap
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.core.util.Helpers.logged import io.bkbn.kompendium.core.util.Helpers.logged
import io.bkbn.kompendium.core.util.Helpers.toNumber import io.bkbn.kompendium.core.util.Helpers.toNumber
@ -29,6 +31,7 @@ import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.ReferencedSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema import io.bkbn.kompendium.oas.schema.SimpleSchema
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KClassifier import kotlin.reflect.KClassifier
@ -36,6 +39,7 @@ import kotlin.reflect.KProperty1
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
@ -144,6 +148,10 @@ object Kontent {
false -> { false -> {
logger.debug("$slug was not found in cache, generating now") logger.debug("$slug was not found in cache, generating now")
var newCache = cache var newCache = cache
// If referenced, add tie from simple slug to schema slug
if (clazz.hasAnnotation<Referenced>()) {
newCache = newCache.plus(type.getSimpleSlug() to ReferencedSchema(type.getReferenceSlug()))
}
// Grabs any type parameters mapped to the corresponding type argument(s) // Grabs any type parameters mapped to the corresponding type argument(s)
val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap() val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
// associates each member with a Pair of prop name to property schema // associates each member with a Pair of prop name to property schema
@ -274,6 +282,7 @@ object Kontent {
is FreeFormSchema -> this // todo anything here? is FreeFormSchema -> this // todo anything here?
is ObjectSchema -> scanForConstraints(clazz, prop) is ObjectSchema -> scanForConstraints(clazz, prop)
is SimpleSchema -> scanForConstraints(prop) is SimpleSchema -> scanForConstraints(prop)
is ReferencedSchema -> this // todo anything here?
} }
private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema { private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {

View File

@ -1,19 +1,19 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.lang.reflect.ParameterizedType
import java.util.Locale import java.util.Locale
object Helpers { object Helpers {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
private const val COMPONENT_SLUG = "#/components/schemas" const val COMPONENT_SLUG = "#/components/schemas"
val UNIT_TYPE by lazy { Unit::class.createType() } val UNIT_TYPE by lazy { Unit::class.createType() }

View File

@ -46,6 +46,7 @@ import io.bkbn.kompendium.core.util.requiredParameter
import io.bkbn.kompendium.core.util.returnsList import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.rootModule import io.bkbn.kompendium.core.util.rootModule
import io.bkbn.kompendium.core.util.simpleGenericResponse import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.simpleRecursive
import io.bkbn.kompendium.core.util.trailingSlash import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.undeclaredType import io.bkbn.kompendium.core.util.undeclaredType
import io.bkbn.kompendium.core.util.uniqueArray import io.bkbn.kompendium.core.util.uniqueArray
@ -213,6 +214,9 @@ class KompendiumTest : DescribeSpec({
it("Can override field values via annotation") { it("Can override field values via annotation") {
openApiTest("field_override.json") { overrideFieldInfo() } openApiTest("field_override.json") { overrideFieldInfo() }
} }
it("Can serialize a recursive type using references") {
openApiTest("simple_recursive.json") { simpleRecursive() }
}
} }
describe("Constraints") { describe("Constraints") {
it("Can set a minimum and maximum integer value") { it("Can set a minimum and maximum integer value") {

View File

@ -405,6 +405,16 @@ fun Application.overrideFieldInfo() {
} }
} }
fun Application.simpleRecursive() {
routing {
route("/test/simple_recursive") {
notarizedGet(TestResponseInfo.simpleRecursive) {
call.respond(HttpStatusCode.OK)
}
}
}
}
fun Application.constrainedIntInfo() { fun Application.constrainedIntInfo() {
routing { routing {
route("/test/constrained_int") { route("/test/constrained_int") {

View File

@ -119,6 +119,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -196,6 +196,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -80,6 +80,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -68,6 +68,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -90,6 +90,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -126,6 +126,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -58,6 +58,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -68,6 +68,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -58,6 +58,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -114,6 +114,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -60,6 +60,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -59,6 +59,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -59,6 +59,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -59,6 +59,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -56,6 +56,7 @@
} }
}, },
"components" : { "components" : {
"schemas": {},
"securitySchemes" : { } "securitySchemes" : { }
}, },
"security" : [ ], "security" : [ ],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -95,6 +95,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -113,6 +113,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -78,6 +78,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -42,6 +42,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -57,6 +57,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -122,6 +122,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -61,6 +61,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -122,6 +122,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -67,6 +67,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -115,6 +115,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -80,6 +80,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -80,6 +80,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -92,6 +92,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -88,6 +88,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -58,6 +58,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -67,6 +67,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -80,6 +80,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,45 @@
"b" "b"
], ],
"type": "object" "type": "object"
},
{
"properties": {
"c": {
"anyOf": [
{
"properties": {
"a": {
"format": "int32",
"type": "integer"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"format": "float",
"type": "number"
}
},
"required": [
"b"
],
"type": "object"
},
{
"$ref": "#/components/schemas/InsaneJamma"
}
]
}
},
"required": [
"c"
],
"type": "object"
} }
] ]
} }
@ -74,6 +113,47 @@
} }
}, },
"components": { "components": {
"schemas": {
"InsaneJamma": {
"properties": {
"c": {
"anyOf": [
{
"properties": {
"a": {
"format": "int32",
"type": "integer"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"format": "float",
"type": "number"
}
},
"required": [
"b"
],
"type": "object"
},
{
"$ref": "#/components/schemas/InsaneJamma"
}
]
}
},
"required": [
"c"
],
"type": "object"
}
},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -0,0 +1,123 @@
{
"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": {
"/test/simple_recursive": {
"get": {
"tags": [],
"summary": "Simple recursive example",
"description": "Pretty neato!",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"description": {
"type": "string"
},
"mode": {
"enum": [
"NULLABLE",
"REQUIRED",
"REPEATED"
],
"type": "string"
},
"name": {
"type": "string"
},
"subColumns": {
"items": {
"$ref": "#/components/schemas/ColumnSchema"
},
"type": "array"
},
"type": {
"type": "string"
}
},
"required": [
"name",
"type",
"description",
"mode"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"ColumnSchema": {
"properties": {
"description": {
"type": "string"
},
"mode": {
"enum": [
"NULLABLE",
"REQUIRED",
"REPEATED"
],
"type": "string"
},
"name": {
"type": "string"
},
"subColumns": {
"items": {
"$ref": "#/components/schemas/ColumnSchema"
},
"type": "array"
},
"type": {
"type": "string"
}
},
"required": [
"name",
"type",
"description",
"mode"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -77,6 +77,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -64,6 +64,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -62,6 +62,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -4,6 +4,7 @@ import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.FreeFormObject import io.bkbn.kompendium.annotations.FreeFormObject
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.Referenced
import io.bkbn.kompendium.annotations.UndeclaredField import io.bkbn.kompendium.annotations.UndeclaredField
import io.bkbn.kompendium.annotations.constraint.Format import io.bkbn.kompendium.annotations.constraint.Format
import io.bkbn.kompendium.annotations.constraint.MaxItems import io.bkbn.kompendium.annotations.constraint.MaxItems
@ -198,7 +199,9 @@ sealed interface SlammaJamma
data class OneJamma(val a: Int) : SlammaJamma data class OneJamma(val a: Int) : SlammaJamma
data class AnothaJamma(val b: Float) : SlammaJamma data class AnothaJamma(val b: Float) : SlammaJamma
//data class InsaneJamma(val c: SlammaJamma) : SlammaJamma // 👀
@Referenced
data class InsaneJamma(val c: SlammaJamma) : SlammaJamma
sealed interface Flibbity<T> sealed interface Flibbity<T>
@ -216,3 +219,18 @@ data class Mysterious(val nowYouSeeMe: String)
data class HeaderNameTest( data class HeaderNameTest(
@Param(type = ParamType.HEADER) val `X-UserEmail`: String @Param(type = ParamType.HEADER) val `X-UserEmail`: String
) )
enum class ColumnMode {
NULLABLE,
REQUIRED,
REPEATED
}
@Referenced
data class ColumnSchema(
val name: String,
val type: String,
val description: String,
val mode: ColumnMode,
val subColumns: List<ColumnSchema> = emptyList()
)

View File

@ -2,8 +2,6 @@ package io.bkbn.kompendium.core.fixtures
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
@ -11,6 +9,8 @@ import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo import io.bkbn.kompendium.core.metadata.method.HeadInfo
import io.bkbn.kompendium.core.metadata.method.OptionsInfo import io.bkbn.kompendium.core.metadata.method.OptionsInfo
import io.bkbn.kompendium.core.metadata.method.PatchInfo import io.bkbn.kompendium.core.metadata.method.PatchInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -169,6 +169,12 @@ object TestResponseInfo {
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val simpleRecursive = GetInfo<Unit, ColumnSchema>(
summary = "Simple recursive example",
description = "Pretty neato!",
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",

View File

@ -76,6 +76,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -67,6 +67,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -76,6 +76,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -67,6 +67,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -95,6 +95,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -86,6 +86,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -95,6 +95,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -86,6 +86,7 @@
} }
}, },
"components": { "components": {
"schemas": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"security": [], "security": [],

View File

@ -1,9 +1,11 @@
package io.bkbn.kompendium.oas.component package io.bkbn.kompendium.oas.component
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.security.SecuritySchema import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Components( data class Components(
val schemas: MutableMap<String, ComponentSchema> = mutableMapOf(),
val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf() val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf()
) )

View File

@ -20,6 +20,8 @@ sealed interface ComponentSchema {
is FormattedSchema -> this.copy(default = default) is FormattedSchema -> this.copy(default = default)
is ObjectSchema -> this.copy(default = default) is ObjectSchema -> this.copy(default = default)
is SimpleSchema -> this.copy(default = default) is SimpleSchema -> this.copy(default = default)
is ReferencedSchema -> this.copy(default = default)
is FreeFormSchema -> this.copy(default = default)
else -> error("Compiler bug??") else -> error("Compiler bug??")
} }
@ -31,6 +33,8 @@ sealed interface ComponentSchema {
is FormattedSchema -> this.copy(description = description) is FormattedSchema -> this.copy(description = description)
is ObjectSchema -> this.copy(description = description) is ObjectSchema -> this.copy(description = description)
is SimpleSchema -> this.copy(description = description) is SimpleSchema -> this.copy(description = description)
is ReferencedSchema -> this.copy(description = description)
is FreeFormSchema -> this.copy(description = description)
else -> error("Compiler bug??") else -> error("Compiler bug??")
} }
} }

View File

@ -8,9 +8,10 @@ data class FreeFormSchema(
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints
val minProperties: Int? = null, val minProperties: Int? = null,
val maxProperties: Int? = null val maxProperties: Int? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
) : TypedSchema { ) : TypedSchema {
val additionalProperties: Boolean = true val additionalProperties: Boolean = true
override val type: String = "object" override val type: String = "object"
override val default: @Contextual Any? = null
} }

View File

@ -0,0 +1,11 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ReferencedSchema(
val `$ref`: String,
override val default: @Contextual Any? = null,
override val description: String? = null
) : ComponentSchema

View File

@ -8,6 +8,7 @@ import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.ReferencedSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema import io.bkbn.kompendium.oas.schema.SimpleSchema
import io.bkbn.kompendium.oas.security.ApiKeyAuth import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth import io.bkbn.kompendium.oas.security.BasicAuth
@ -29,6 +30,7 @@ object KompendiumSerializersModule {
subclass(DictionarySchema::class, DictionarySchema.serializer()) subclass(DictionarySchema::class, DictionarySchema.serializer())
subclass(EnumSchema::class, EnumSchema.serializer()) subclass(EnumSchema::class, EnumSchema.serializer())
subclass(FreeFormSchema::class, FreeFormSchema.serializer()) subclass(FreeFormSchema::class, FreeFormSchema.serializer())
subclass(ReferencedSchema::class, ReferencedSchema.serializer())
} }
polymorphic(SecuritySchema::class) { polymorphic(SecuritySchema::class) {
subclass(ApiKeyAuth::class, ApiKeyAuth.serializer()) subclass(ApiKeyAuth::class, ApiKeyAuth.serializer())

View File

@ -0,0 +1,97 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.annotations.Referenced
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
enum class ColumnMode {
NULLABLE,
REQUIRED,
REPEATED
}
@Referenced // Indicates that Kompendium should store this class as a $ref component.
@Serializable
data class ColumnSchema(
val name: String,
val type: String,
val description: String,
val mode: ColumnMode,
val subColumns: List<ColumnSchema> = emptyList()
)
sealed interface RecursiveSlammaJamma
@Serializable
data class OneJamma(val a: Int) : RecursiveSlammaJamma
@Serializable
data class AnothaJamma(val b: Float) : RecursiveSlammaJamma
@Referenced
@Serializable
data class InsaneJamma(val c: RecursiveSlammaJamma) : RecursiveSlammaJamma
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
json(json = Util.kotlinxConfig)
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc(pageTitle = "Recursive API Docs")
notarizedGet(
GetInfo<Unit, ColumnSchema>(
summary = "Its recursive",
description = "This is how we do it!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
),
tags = setOf("Simple")
)
) {
call.respond(HttpStatusCode.OK, "Nice!")
}
route("cmon_and_slam") {
notarizedGet(
GetInfo<Unit, RecursiveSlammaJamma>(
summary = "Its recursive",
description = "This is how we do it!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
),
tags = setOf("Simple")
)
) {
call.respond(HttpStatusCode.OK, "Nice!")
}
}
}
}