major reflection refactor (#15)
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.0] - April 16th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Completely redid the reflection system to improve flow, decrease errors ✨
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added ReDoc to the Playground to make manual testing more convenient
|
||||||
|
|
||||||
## [0.0.7] - April 16th, 2021
|
## [0.0.7] - April 16th, 2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=0.0.7
|
project.version=0.1.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -9,11 +9,12 @@ logback = "1.2.3"
|
|||||||
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
|
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
|
||||||
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
|
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
|
||||||
ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" }
|
ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" }
|
||||||
|
ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" }
|
||||||
# Logging
|
# Logging
|
||||||
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
||||||
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
|
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson" ]
|
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
|
||||||
logging = [ "slf4j", "logback-classic", "logback-core" ]
|
logging = [ "slf4j", "logback-classic", "logback-core" ]
|
||||||
|
@ -6,6 +6,7 @@ import io.ktor.routing.Route
|
|||||||
import io.ktor.routing.method
|
import io.ktor.routing.method
|
||||||
import io.ktor.util.pipeline.PipelineInterceptor
|
import io.ktor.util.pipeline.PipelineInterceptor
|
||||||
import kotlin.reflect.full.findAnnotation
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import org.leafygreens.kompendium.Kontent.generateKontent
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||||
@ -17,14 +18,11 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
|
|||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
|
||||||
|
import org.leafygreens.kompendium.util.Helpers.COMPONENT_SLUG
|
||||||
import org.leafygreens.kompendium.util.Helpers.calculatePath
|
import org.leafygreens.kompendium.util.Helpers.calculatePath
|
||||||
import org.leafygreens.kompendium.util.Helpers.objectSchemaPair
|
|
||||||
import org.leafygreens.kompendium.util.Helpers.putPairIfAbsent
|
|
||||||
|
|
||||||
object Kompendium {
|
object Kompendium {
|
||||||
|
|
||||||
const val COMPONENT_SLUG = "#/components/schemas"
|
|
||||||
|
|
||||||
var openApiSpec = OpenApiSpec(
|
var openApiSpec = OpenApiSpec(
|
||||||
info = OpenApiSpecInfo(),
|
info = OpenApiSpecInfo(),
|
||||||
servers = mutableListOf(),
|
servers = mutableListOf(),
|
||||||
@ -34,7 +32,7 @@ object Kompendium {
|
|||||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
|
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
|
||||||
info: MethodInfo,
|
info: MethodInfo,
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route = generateComponentSchemas<TParam, Unit, TResp>() {
|
): Route = generateComponentSchemas<Unit, TResp>() {
|
||||||
val path = calculatePath()
|
val path = calculatePath()
|
||||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
openApiSpec.paths[path]?.get = info.parseMethodInfo<Unit, TResp>()
|
openApiSpec.paths[path]?.get = info.parseMethodInfo<Unit, TResp>()
|
||||||
@ -44,7 +42,7 @@ object Kompendium {
|
|||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
|
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
|
||||||
info: MethodInfo,
|
info: MethodInfo,
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route = generateComponentSchemas<TParam, TReq, TResp>() {
|
): Route = generateComponentSchemas<TReq, TResp>() {
|
||||||
val path = calculatePath()
|
val path = calculatePath()
|
||||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
openApiSpec.paths[path]?.post = info.parseMethodInfo<TReq, TResp>()
|
openApiSpec.paths[path]?.post = info.parseMethodInfo<TReq, TResp>()
|
||||||
@ -54,7 +52,7 @@ object Kompendium {
|
|||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
|
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
|
||||||
info: MethodInfo,
|
info: MethodInfo,
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||||
): Route = generateComponentSchemas<TParam, TReq, TResp>() {
|
): Route = generateComponentSchemas<TReq, TResp>() {
|
||||||
val path = calculatePath()
|
val path = calculatePath()
|
||||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
openApiSpec.paths[path]?.put = info.parseMethodInfo<TReq, TResp>()
|
openApiSpec.paths[path]?.put = info.parseMethodInfo<TReq, TResp>()
|
||||||
@ -64,7 +62,7 @@ object Kompendium {
|
|||||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
||||||
info: MethodInfo,
|
info: MethodInfo,
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route = generateComponentSchemas<TParam, Unit, TResp> {
|
): Route = generateComponentSchemas<Unit, TResp> {
|
||||||
val path = calculatePath()
|
val path = calculatePath()
|
||||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
openApiSpec.paths[path]?.delete = info.parseMethodInfo<Unit, TResp>()
|
openApiSpec.paths[path]?.delete = info.parseMethodInfo<Unit, TResp>()
|
||||||
@ -80,19 +78,21 @@ object Kompendium {
|
|||||||
requestBody = parseRequestAnnotation<TReq>()
|
requestBody = parseRequestAnnotation<TReq>()
|
||||||
)
|
)
|
||||||
|
|
||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> generateComponentSchemas(
|
inline fun <reified TReq : Any, reified TResp : Any> generateComponentSchemas(
|
||||||
block: () -> Route
|
block: () -> Route
|
||||||
): Route {
|
): Route {
|
||||||
if (TResp::class != Unit::class) openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TResp::class))
|
val responseKontent = generateKontent(TResp::class)
|
||||||
if (TReq::class != Unit::class) openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TReq::class))
|
val requestKontent = generateKontent(TReq::class)
|
||||||
|
openApiSpec.components.schemas.putAll(responseKontent)
|
||||||
|
openApiSpec.components.schemas.putAll(requestKontent)
|
||||||
return block.invoke()
|
return block.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified TReq> parseRequestAnnotation(): OpenApiSpecRequest? = when (TReq::class) {
|
inline fun <reified TReq> parseRequestAnnotation(): OpenApiSpecRequest? = when (TReq::class) {
|
||||||
Unit::class -> null
|
Unit::class -> null
|
||||||
else -> {
|
else -> when (val anny = TReq::class.findAnnotation<KompendiumRequest>()) {
|
||||||
val anny = TReq::class.findAnnotation<KompendiumRequest>() ?: error("My way or the highway bub")
|
null -> null
|
||||||
OpenApiSpecRequest(
|
else -> OpenApiSpecRequest(
|
||||||
description = anny.description,
|
description = anny.description,
|
||||||
content = anny.mediaTypes.associate {
|
content = anny.mediaTypes.associate {
|
||||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TReq::class.simpleName}")
|
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TReq::class.simpleName}")
|
||||||
@ -105,8 +105,9 @@ object Kompendium {
|
|||||||
|
|
||||||
inline fun <reified TResp> parseResponseAnnotation(): Pair<Int, OpenApiSpecResponse>? = when (TResp::class) {
|
inline fun <reified TResp> parseResponseAnnotation(): Pair<Int, OpenApiSpecResponse>? = when (TResp::class) {
|
||||||
Unit::class -> null
|
Unit::class -> null
|
||||||
|
else -> when (val anny = TResp::class.findAnnotation<KompendiumResponse>()) {
|
||||||
|
null -> null
|
||||||
else -> {
|
else -> {
|
||||||
val anny = TResp::class.findAnnotation<KompendiumResponse>() ?: error("My way or the highway bub")
|
|
||||||
val specResponse = OpenApiSpecResponse(
|
val specResponse = OpenApiSpecResponse(
|
||||||
description = anny.description,
|
description = anny.description,
|
||||||
content = anny.mediaTypes.associate {
|
content = anny.mediaTypes.associate {
|
||||||
@ -118,6 +119,7 @@ object Kompendium {
|
|||||||
Pair(anny.status, specResponse)
|
Pair(anny.status, specResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun resetSchema() {
|
internal fun resetSchema() {
|
||||||
openApiSpec = OpenApiSpec(
|
openApiSpec = OpenApiSpec(
|
||||||
|
@ -0,0 +1,126 @@
|
|||||||
|
package org.leafygreens.kompendium
|
||||||
|
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.reflect.full.isSubclassOf
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.jvm.javaField
|
||||||
|
import org.leafygreens.kompendium.models.oas.ArraySchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.DictionarySchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.EnumSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.FormatSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.ObjectSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponentSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.ReferencedSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.SimpleSchema
|
||||||
|
import org.leafygreens.kompendium.util.Helpers.COMPONENT_SLUG
|
||||||
|
import org.leafygreens.kompendium.util.Helpers.genericNameAdapter
|
||||||
|
import org.leafygreens.kompendium.util.Helpers.logged
|
||||||
|
import org.leafygreens.kompendium.util.Helpers.toPair
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
typealias SchemaMap = Map<String, OpenApiSpecComponentSchema>
|
||||||
|
|
||||||
|
object Kontent {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
fun generateKontent(
|
||||||
|
clazz: KClass<*>,
|
||||||
|
cache: SchemaMap = emptyMap()
|
||||||
|
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||||
|
when {
|
||||||
|
clazz == Unit::class -> cache
|
||||||
|
clazz == Int::class -> cache.plus(clazz.simpleName!! to FormatSchema("int32", "integer"))
|
||||||
|
clazz == Long::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
|
||||||
|
clazz == Double::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
|
||||||
|
clazz == Float::class -> cache.plus(clazz.simpleName!! to FormatSchema("float", "number"))
|
||||||
|
clazz == String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
||||||
|
clazz == Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
||||||
|
clazz.isSubclassOf(Enum::class) -> error("Top level enums are currently not supported by Kompendium")
|
||||||
|
clazz.typeParameters.isNotEmpty() -> error("Top level generics are not supported by Kompendium")
|
||||||
|
else -> handleComplexType(clazz, cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleComplexType(clazz: KClass<*>, cache: SchemaMap): SchemaMap =
|
||||||
|
when (cache.containsKey(clazz.simpleName)) {
|
||||||
|
true -> {
|
||||||
|
logger.info("Cache already contains ${clazz.simpleName}, returning cache untouched")
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
false -> {
|
||||||
|
logger.info("${clazz.simpleName} was not found in cache, generating now")
|
||||||
|
var newCache = cache
|
||||||
|
val fieldMap = clazz.memberProperties.associate { prop ->
|
||||||
|
logger.info("Analyzing $prop in class $clazz")
|
||||||
|
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
||||||
|
logger.info("Detected field $field")
|
||||||
|
if (!newCache.containsKey(field.simpleName)) {
|
||||||
|
logger.info("Cache was missing ${field.simpleName}, adding now")
|
||||||
|
newCache = generateFieldKontent(prop, field, newCache)
|
||||||
|
}
|
||||||
|
val propSchema = ReferencedSchema(field.getReferenceSlug(prop))
|
||||||
|
Pair(prop.name, propSchema)
|
||||||
|
}
|
||||||
|
logger.info("${clazz.simpleName} contains $fieldMap")
|
||||||
|
val schema = ObjectSchema(fieldMap)
|
||||||
|
logger.info("${clazz.simpleName} schema: $schema")
|
||||||
|
newCache.plus(clazz.simpleName!! to schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KClass<*>.getReferenceSlug(prop: KProperty<*>): String = when {
|
||||||
|
this.typeParameters.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, prop)}"
|
||||||
|
else -> "$COMPONENT_SLUG/${simpleName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateFieldKontent(
|
||||||
|
prop: KProperty<*>,
|
||||||
|
field: KClass<*>,
|
||||||
|
cache: SchemaMap
|
||||||
|
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||||
|
when {
|
||||||
|
field.isSubclassOf(Enum::class) -> enumFieldHandler(prop, field, cache)
|
||||||
|
field.isSubclassOf(Map::class) -> mapFieldHandler(prop, field, cache)
|
||||||
|
field.isSubclassOf(Collection::class) -> collectionFieldHandler(prop, field, cache)
|
||||||
|
else -> generateKontent(field, cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enumFieldHandler(prop: KProperty<*>, field: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
logger.info("Enum detected for $prop, gathering values")
|
||||||
|
val options = prop.javaField?.type?.enumConstants?.map { it.toString() }?.toSet()
|
||||||
|
?: error("unable to parse enum $prop")
|
||||||
|
return cache.plus(field.simpleName!! to EnumSchema(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapFieldHandler(prop: KProperty<*>, field: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
logger.info("Map detected for $prop, generating schema and appending to cache")
|
||||||
|
val (keyClass, valClass) = (prop.javaField?.genericType as ParameterizedType)
|
||||||
|
.actualTypeArguments.slice(IntRange(0, 1))
|
||||||
|
.map { it as Class<*> }
|
||||||
|
.map { it.kotlin }
|
||||||
|
.toPair()
|
||||||
|
if (keyClass != String::class) error("Invalid Map $prop: OpenAPI dictionaries must have keys of type String")
|
||||||
|
val referenceName = genericNameAdapter(field, prop)
|
||||||
|
val valueReference = ReferencedSchema("$COMPONENT_SLUG/${valClass.simpleName}")
|
||||||
|
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||||
|
val updatedCache = generateKontent(valClass, cache)
|
||||||
|
return updatedCache.plus(referenceName to schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectionFieldHandler(prop: KProperty<*>, field: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
logger.info("Collection detected for $prop, generating schema and appending to cache")
|
||||||
|
val collectionClass = ((prop.javaField?.genericType as ParameterizedType)
|
||||||
|
.actualTypeArguments.first() as Class<*>).kotlin
|
||||||
|
logger.info("Obtained collection class: $collectionClass")
|
||||||
|
val referenceName = genericNameAdapter(field, prop)
|
||||||
|
val valueReference = ReferencedSchema("$COMPONENT_SLUG/${collectionClass.simpleName}")
|
||||||
|
val schema = ArraySchema(items = valueReference)
|
||||||
|
val updatedCache = generateKontent(collectionClass, cache)
|
||||||
|
return updatedCache.plus(referenceName to schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,22 +1,26 @@
|
|||||||
package org.leafygreens.kompendium.models.oas
|
package org.leafygreens.kompendium.models.oas
|
||||||
|
|
||||||
// TODO Enum for type?
|
// TODO Enum for type?
|
||||||
sealed class OpenApiSpecComponentSchema(open val type: String)
|
sealed class OpenApiSpecComponentSchema
|
||||||
|
|
||||||
|
sealed class TypedSchema(open val type: String) : OpenApiSpecComponentSchema()
|
||||||
|
|
||||||
|
data class ReferencedSchema(val `$ref`: String) : OpenApiSpecComponentSchema()
|
||||||
|
|
||||||
data class ObjectSchema(
|
data class ObjectSchema(
|
||||||
val properties: Map<String, OpenApiSpecComponentSchema>
|
val properties: Map<String, OpenApiSpecComponentSchema>
|
||||||
) : OpenApiSpecComponentSchema("object")
|
) : TypedSchema("object")
|
||||||
|
|
||||||
data class DictionarySchema(
|
data class DictionarySchema(
|
||||||
val additionalProperties: OpenApiSpecComponentSchema
|
val additionalProperties: OpenApiSpecComponentSchema
|
||||||
) : OpenApiSpecComponentSchema("object")
|
) : TypedSchema("object")
|
||||||
|
|
||||||
data class EnumSchema(
|
data class EnumSchema(
|
||||||
val `enum`: Set<String>
|
val `enum`: Set<String>
|
||||||
) : OpenApiSpecComponentSchema("string")
|
) : TypedSchema("string")
|
||||||
|
|
||||||
data class SimpleSchema(override val type: String) : OpenApiSpecComponentSchema(type)
|
data class SimpleSchema(override val type: String) : TypedSchema(type)
|
||||||
|
|
||||||
data class FormatSchema(val format: String, override val type: String) : OpenApiSpecComponentSchema(type)
|
data class FormatSchema(val format: String, override val type: String) : TypedSchema(type)
|
||||||
|
|
||||||
data class ArraySchema(val items: OpenApiSpecComponentSchema) : OpenApiSpecComponentSchema("array")
|
data class ArraySchema(val items: OpenApiSpecComponentSchema) : TypedSchema("array")
|
||||||
|
@ -12,6 +12,7 @@ import kotlin.reflect.full.findAnnotation
|
|||||||
import kotlin.reflect.full.isSubclassOf
|
import kotlin.reflect.full.isSubclassOf
|
||||||
import kotlin.reflect.full.memberProperties
|
import kotlin.reflect.full.memberProperties
|
||||||
import kotlin.reflect.jvm.javaField
|
import kotlin.reflect.jvm.javaField
|
||||||
|
import org.leafygreens.kompendium.Kontent
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||||
import org.leafygreens.kompendium.models.oas.ArraySchema
|
import org.leafygreens.kompendium.models.oas.ArraySchema
|
||||||
import org.leafygreens.kompendium.models.oas.DictionarySchema
|
import org.leafygreens.kompendium.models.oas.DictionarySchema
|
||||||
@ -26,6 +27,8 @@ object Helpers {
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
const val COMPONENT_SLUG = "#/components/schemas"
|
||||||
|
|
||||||
@OptIn(InternalAPI::class)
|
@OptIn(InternalAPI::class)
|
||||||
fun Route.calculatePath(tail: String = ""): String {
|
fun Route.calculatePath(tail: String = ""): String {
|
||||||
logger.info("Building path for ${selector::class}")
|
logger.info("Building path for ${selector::class}")
|
||||||
@ -67,83 +70,27 @@ object Helpers {
|
|||||||
|
|
||||||
fun <K, V> MutableMap<K, V>.putPairIfAbsent(pair: Pair<K, V>) = putIfAbsent(pair.first, pair.second)
|
fun <K, V> MutableMap<K, V>.putPairIfAbsent(pair: Pair<K, V>) = putIfAbsent(pair.first, pair.second)
|
||||||
|
|
||||||
// TODO Investigate a caching mechanism to reduce overhead... then just reference once created
|
fun <T> List<T>.toPair(): Pair<T, T> {
|
||||||
fun objectSchemaPair(clazz: KClass<*>): Pair<String, ObjectSchema> {
|
|
||||||
logger.info("Generating object schema for ${clazz.simpleName}")
|
|
||||||
val o = objectSchema(clazz)
|
|
||||||
return Pair(clazz.simpleName!!, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun objectSchema(clazz: KClass<*>): ObjectSchema =
|
|
||||||
ObjectSchema(properties = clazz.memberProperties.associate { prop ->
|
|
||||||
logger.info("Analyzing $prop in class $clazz")
|
|
||||||
val field = prop.javaField?.type?.kotlin
|
|
||||||
val anny = prop.findAnnotation<KompendiumField>()
|
|
||||||
|
|
||||||
if (anny != null) logger.info("Found field annotation: $anny")
|
|
||||||
|
|
||||||
|
|
||||||
val schema = when {
|
|
||||||
field?.isSubclassOf(Enum::class) == true -> {
|
|
||||||
logger.info("Detected that $prop is an enum")
|
|
||||||
val options = prop.javaField?.type?.enumConstants?.map { it.toString() }?.toSet()
|
|
||||||
?: error("unable to parse enum $prop")
|
|
||||||
EnumSchema(options)
|
|
||||||
}
|
|
||||||
field?.isSubclassOf(Map::class) == true || field?.isSubclassOf(Map.Entry::class) == true -> {
|
|
||||||
logger.info("$prop is a Map, doing some crazy stuff")
|
|
||||||
mapFieldSchema(prop)
|
|
||||||
}
|
|
||||||
field?.isSubclassOf(Collection::class) == true -> {
|
|
||||||
logger.info("$prop is a List, building array schema")
|
|
||||||
listFieldSchema(prop)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logger.info("$prop is not a list or map, going directly to schema detection")
|
|
||||||
fieldToSchema(field as KClass<*>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val name = anny?.let {
|
|
||||||
logger.info("Overriding property name with annotation $anny")
|
|
||||||
anny.name
|
|
||||||
} ?: prop.name
|
|
||||||
|
|
||||||
Pair(name, schema)
|
|
||||||
})
|
|
||||||
|
|
||||||
private fun mapFieldSchema(prop: KProperty<*>): DictionarySchema {
|
|
||||||
val (keyType, valType) = (prop.javaField?.genericType as ParameterizedType)
|
|
||||||
.actualTypeArguments.slice(IntRange(0, 1))
|
|
||||||
.map { it as Class<*> }
|
|
||||||
.map { it.kotlin }
|
|
||||||
.toPair()
|
|
||||||
if (keyType != String::class) error("Invalid Map $prop: OpenAPI dictionaries must have keys of type String")
|
|
||||||
return DictionarySchema(additionalProperties = fieldToSchema(valType))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun listFieldSchema(prop: KProperty<*>): ArraySchema {
|
|
||||||
val listType = ((prop.javaField?.genericType
|
|
||||||
as ParameterizedType).actualTypeArguments.first()
|
|
||||||
as Class<*>).kotlin
|
|
||||||
logger.info("Obtained List type, converting to schema $listType")
|
|
||||||
return ArraySchema(fieldToSchema(listType))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fieldToSchema(field: KClass<*>): OpenApiSpecComponentSchema = when (field) {
|
|
||||||
Int::class -> FormatSchema("int32", "integer")
|
|
||||||
Long::class -> FormatSchema("int64", "integer")
|
|
||||||
Double::class -> FormatSchema("double", "number")
|
|
||||||
Float::class -> FormatSchema("float", "number")
|
|
||||||
String::class -> SimpleSchema("string")
|
|
||||||
Boolean::class -> SimpleSchema("boolean")
|
|
||||||
else -> objectSchema(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> List<T>.toPair(): Pair<T, T> {
|
|
||||||
if (this.size != 2) {
|
if (this.size != 2) {
|
||||||
throw IllegalArgumentException("List is not of length 2!")
|
throw IllegalArgumentException("List is not of length 2!")
|
||||||
}
|
}
|
||||||
return Pair(this[0], this[1])
|
return Pair(this[0], this[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun genericNameAdapter(field: KClass<*>, prop: KProperty<*>): String {
|
||||||
|
val typeArgs = (prop.javaField?.genericType as ParameterizedType).actualTypeArguments
|
||||||
|
val classNames = typeArgs.map { it as Class<*> }.map { it.kotlin }.map { it.simpleName }
|
||||||
|
return classNames.joinToString(separator = "-", prefix = "${field.simpleName}-")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Higher order function that takes a map of names to objects and will log their state ahead of function invocation
|
||||||
|
* along with the result of the function invocation
|
||||||
|
*/
|
||||||
|
fun <T> logged(functionName: String, entities: Map<String, Any>, block: () -> T): T {
|
||||||
|
entities.forEach { (name, entity) -> logger.info("Ahead of $functionName invocation, $name: $entity") }
|
||||||
|
val result = block.invoke()
|
||||||
|
logger.info("Result of $functionName invocation: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,6 +273,22 @@ internal class KompendiumTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Can notarize primitives`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
openApiModule()
|
||||||
|
primitives()
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_primitives.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val testGetInfo = MethodInfo("Another get test", "testing more")
|
val testGetInfo = MethodInfo("Another get test", "testing more")
|
||||||
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
||||||
@ -381,6 +397,16 @@ internal class KompendiumTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Application.primitives() {
|
||||||
|
routing {
|
||||||
|
route("/test") {
|
||||||
|
notarizedPut<Unit, Int, Boolean>(testPutInfo) {
|
||||||
|
call.respondText { "heya" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Application.openApiModule() {
|
private fun Application.openApiModule() {
|
||||||
routing {
|
routing {
|
||||||
route("/openapi.json") {
|
route("/openapi.json") {
|
||||||
|
@ -0,0 +1,169 @@
|
|||||||
|
package org.leafygreens.kompendium
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import org.leafygreens.kompendium.Kontent.generateKontent
|
||||||
|
import org.leafygreens.kompendium.models.oas.FormatSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.ObjectSchema
|
||||||
|
import org.leafygreens.kompendium.models.oas.ReferencedSchema
|
||||||
|
import org.leafygreens.kompendium.util.TestInvalidMap
|
||||||
|
import org.leafygreens.kompendium.util.TestNestedModel
|
||||||
|
import org.leafygreens.kompendium.util.TestSimpleModel
|
||||||
|
import org.leafygreens.kompendium.util.TestSimpleWithEnumList
|
||||||
|
import org.leafygreens.kompendium.util.TestSimpleWithEnums
|
||||||
|
import org.leafygreens.kompendium.util.TestSimpleWithList
|
||||||
|
import org.leafygreens.kompendium.util.TestSimpleWithMap
|
||||||
|
|
||||||
|
internal class KontentTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Unit returns empty map on generate`() {
|
||||||
|
// when
|
||||||
|
val clazz = Unit::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertTrue { result.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Primitive types return a single map result`() {
|
||||||
|
// when
|
||||||
|
val clazz = Long::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertEquals(1, result.count(), "Should have a single result")
|
||||||
|
assertEquals(FormatSchema("int64", "integer"), result["Long"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Throws an error when top level generics are detected`() {
|
||||||
|
// when
|
||||||
|
val womp = mapOf("asdf" to "fdsa", "2cool" to "4school")
|
||||||
|
val clazz = womp::class
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertFailsWith<IllegalStateException> { generateKontent(clazz) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Objects reference their base types in the cache`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestSimpleModel::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(3, result.count())
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generation works for nested object types`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestNestedModel::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(4, result.count())
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
assertTrue { result.containsKey(TestSimpleModel::class.simpleName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generation does not repeat for cached items`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestNestedModel::class
|
||||||
|
val initialCache = generateKontent(clazz)
|
||||||
|
val claxx = TestSimpleModel::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(claxx, initialCache)
|
||||||
|
|
||||||
|
// expect TODO Spy to check invocation count?
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(4, result.count())
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
assertTrue { result.containsKey(TestSimpleModel::class.simpleName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generation allows for enum fields`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestSimpleWithEnums::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(3, result.count())
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generation allows for map fields`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestSimpleWithMap::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(5, result.count())
|
||||||
|
assertTrue { result.containsKey("Map-String-TestSimpleModel") }
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
|
||||||
|
val os = result[clazz.simpleName] as ObjectSchema
|
||||||
|
val expectedRef = ReferencedSchema("#/components/schemas/Map-String-TestSimpleModel")
|
||||||
|
assertEquals(expectedRef, os.properties["b"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `map fields that are not string result in error`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestInvalidMap::class
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertFailsWith<IllegalStateException> { generateKontent(clazz) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generation allows for collection fields`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestSimpleWithList::class
|
||||||
|
|
||||||
|
// do
|
||||||
|
val result = generateKontent(clazz)
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(6, result.count())
|
||||||
|
assertTrue { result.containsKey("List-TestSimpleModel") }
|
||||||
|
assertTrue { result.containsKey(clazz.simpleName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generics as enums throws an exception`() {
|
||||||
|
// when
|
||||||
|
val clazz = TestSimpleWithEnumList::class
|
||||||
|
|
||||||
|
// expect
|
||||||
|
assertFailsWith<java.lang.IllegalStateException> { generateKontent(clazz) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
package org.leafygreens.kompendium.util
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import org.leafygreens.kompendium.util.Helpers.objectSchemaPair
|
|
||||||
|
|
||||||
internal class HelpersTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `can build an object schema from a complex object`() {
|
|
||||||
// when
|
|
||||||
val clazz = ComplexRequest::class
|
|
||||||
|
|
||||||
// do
|
|
||||||
val result = objectSchemaPair(clazz)
|
|
||||||
|
|
||||||
// expect
|
|
||||||
assertNotNull(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -4,6 +4,20 @@ import org.leafygreens.kompendium.annotations.KompendiumField
|
|||||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||||
|
|
||||||
|
data class TestSimpleModel(val a: String, val b: Int)
|
||||||
|
|
||||||
|
data class TestNestedModel(val inner: TestSimpleModel)
|
||||||
|
|
||||||
|
data class TestSimpleWithEnums(val a: String, val b: SimpleEnum)
|
||||||
|
|
||||||
|
data class TestSimpleWithMap(val a: String, val b: Map<String, TestSimpleModel>)
|
||||||
|
|
||||||
|
data class TestSimpleWithList(val a: Boolean, val b: List<TestSimpleModel>)
|
||||||
|
|
||||||
|
data class TestSimpleWithEnumList(val a: Double, val b: List<SimpleEnum>)
|
||||||
|
|
||||||
|
data class TestInvalidMap(val a: Map<Int, TestSimpleModel>)
|
||||||
|
|
||||||
data class TestParams(val a: String, val aa: Int)
|
data class TestParams(val a: String, val aa: Int)
|
||||||
|
|
||||||
data class TestNested(val nesty: String)
|
data class TestNested(val nesty: String)
|
||||||
|
@ -57,44 +57,62 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
"TestResponse" : {
|
"TestResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
},
|
},
|
||||||
|
"SimpleEnum" : {
|
||||||
|
"enum" : [ "ONE", "TWO" ],
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"CrazyItem" : {
|
||||||
|
"properties" : {
|
||||||
|
"enumeration" : {
|
||||||
|
"$ref" : "#/components/schemas/SimpleEnum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Map-String-CrazyItem" : {
|
||||||
|
"additionalProperties" : {
|
||||||
|
"$ref" : "#/components/schemas/CrazyItem"
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"NestedComplexItem" : {
|
||||||
|
"properties" : {
|
||||||
|
"alias" : {
|
||||||
|
"$ref" : "#/components/schemas/Map-String-CrazyItem"
|
||||||
|
},
|
||||||
|
"name" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"List-NestedComplexItem" : {
|
||||||
|
"items" : {
|
||||||
|
"$ref" : "#/components/schemas/NestedComplexItem"
|
||||||
|
},
|
||||||
|
"type" : "array"
|
||||||
|
},
|
||||||
"ComplexRequest" : {
|
"ComplexRequest" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"amazing_field" : {
|
"amazingField" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
},
|
},
|
||||||
"org" : {
|
"org" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
},
|
},
|
||||||
"tables" : {
|
"tables" : {
|
||||||
"items" : {
|
"$ref" : "#/components/schemas/List-NestedComplexItem"
|
||||||
"properties" : {
|
|
||||||
"alias" : {
|
|
||||||
"additionalProperties" : {
|
|
||||||
"properties" : {
|
|
||||||
"enumeration" : {
|
|
||||||
"enum" : [ "ONE", "TWO" ],
|
|
||||||
"type" : "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"name" : {
|
|
||||||
"type" : "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"type" : "array"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -46,10 +46,13 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
"TestResponse" : {
|
"TestResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -57,14 +57,42 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
},
|
||||||
"TestCreatedResponse" : {
|
"TestCreatedResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
},
|
},
|
||||||
"id" : {
|
"id" : {
|
||||||
"format" : "int32",
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Long" : {
|
||||||
|
"format" : "int64",
|
||||||
"type" : "integer"
|
"type" : "integer"
|
||||||
|
},
|
||||||
|
"List-Long" : {
|
||||||
|
"items" : {
|
||||||
|
"$ref" : "#/components/schemas/Long"
|
||||||
|
},
|
||||||
|
"type" : "array"
|
||||||
|
},
|
||||||
|
"Double" : {
|
||||||
|
"format" : "double",
|
||||||
|
"type" : "number"
|
||||||
|
},
|
||||||
|
"TestNested" : {
|
||||||
|
"properties" : {
|
||||||
|
"nesty" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
@ -72,23 +100,13 @@
|
|||||||
"TestRequest" : {
|
"TestRequest" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"aaa" : {
|
"aaa" : {
|
||||||
"items" : {
|
"$ref" : "#/components/schemas/List-Long"
|
||||||
"format" : "int64",
|
|
||||||
"type" : "integer"
|
|
||||||
},
|
|
||||||
"type" : "array"
|
|
||||||
},
|
},
|
||||||
"b" : {
|
"b" : {
|
||||||
"format" : "double",
|
"$ref" : "#/components/schemas/Double"
|
||||||
"type" : "number"
|
|
||||||
},
|
},
|
||||||
"field_name" : {
|
"fieldName" : {
|
||||||
"properties" : {
|
"$ref" : "#/components/schemas/TestNested"
|
||||||
"nesty" : {
|
|
||||||
"type" : "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
49
kompendium-core/src/test/resources/notarized_primitives.json
Normal file
49
kompendium-core/src/test/resources/notarized_primitives.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : {
|
||||||
|
"title" : "Test API",
|
||||||
|
"version" : "1.33.7",
|
||||||
|
"description" : "An amazing, fully-ish \uD83D\uDE09 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/lg-backbone/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" : {
|
||||||
|
"put" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Test put endpoint",
|
||||||
|
"description" : "Put your tests here!",
|
||||||
|
"deprecated" : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"Boolean" : {
|
||||||
|
"type" : "boolean"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : { }
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -57,14 +57,42 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
},
|
||||||
"TestCreatedResponse" : {
|
"TestCreatedResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
},
|
},
|
||||||
"id" : {
|
"id" : {
|
||||||
"format" : "int32",
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Long" : {
|
||||||
|
"format" : "int64",
|
||||||
"type" : "integer"
|
"type" : "integer"
|
||||||
|
},
|
||||||
|
"List-Long" : {
|
||||||
|
"items" : {
|
||||||
|
"$ref" : "#/components/schemas/Long"
|
||||||
|
},
|
||||||
|
"type" : "array"
|
||||||
|
},
|
||||||
|
"Double" : {
|
||||||
|
"format" : "double",
|
||||||
|
"type" : "number"
|
||||||
|
},
|
||||||
|
"TestNested" : {
|
||||||
|
"properties" : {
|
||||||
|
"nesty" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
@ -72,23 +100,13 @@
|
|||||||
"TestRequest" : {
|
"TestRequest" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"aaa" : {
|
"aaa" : {
|
||||||
"items" : {
|
"$ref" : "#/components/schemas/List-Long"
|
||||||
"format" : "int64",
|
|
||||||
"type" : "integer"
|
|
||||||
},
|
|
||||||
"type" : "array"
|
|
||||||
},
|
},
|
||||||
"b" : {
|
"b" : {
|
||||||
"format" : "double",
|
"$ref" : "#/components/schemas/Double"
|
||||||
"type" : "number"
|
|
||||||
},
|
},
|
||||||
"field_name" : {
|
"fieldName" : {
|
||||||
"properties" : {
|
"$ref" : "#/components/schemas/TestNested"
|
||||||
"nesty" : {
|
|
||||||
"type" : "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -46,10 +46,13 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
"TestResponse" : {
|
"TestResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -46,10 +46,13 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
"TestResponse" : {
|
"TestResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -46,10 +46,13 @@
|
|||||||
},
|
},
|
||||||
"components" : {
|
"components" : {
|
||||||
"schemas" : {
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
"TestResponse" : {
|
"TestResponse" : {
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"c" : {
|
"c" : {
|
||||||
"type" : "string"
|
"$ref" : "#/components/schemas/String"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type" : "object"
|
"type" : "object"
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
package org.leafygreens.kompendium.playground
|
package org.leafygreens.kompendium.playground
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import io.ktor.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.features.ContentNegotiation
|
import io.ktor.features.ContentNegotiation
|
||||||
|
import io.ktor.html.respondHtml
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.jackson.jackson
|
import io.ktor.jackson.jackson
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondText
|
import io.ktor.response.respondText
|
||||||
|
import io.ktor.routing.Routing
|
||||||
import io.ktor.routing.get
|
import io.ktor.routing.get
|
||||||
import io.ktor.routing.route
|
import io.ktor.routing.route
|
||||||
import io.ktor.routing.routing
|
import io.ktor.routing.routing
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.server.netty.Netty
|
import io.ktor.server.netty.Netty
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import kotlinx.html.body
|
||||||
|
import kotlinx.html.head
|
||||||
|
import kotlinx.html.link
|
||||||
|
import kotlinx.html.meta
|
||||||
|
import kotlinx.html.script
|
||||||
|
import kotlinx.html.style
|
||||||
|
import kotlinx.html.title
|
||||||
|
import kotlinx.html.unsafe
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
||||||
@ -76,9 +88,14 @@ object KompendiumTOC {
|
|||||||
|
|
||||||
fun Application.mainModule() {
|
fun Application.mainModule() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
jackson()
|
jackson {
|
||||||
|
enable(SerializationFeature.INDENT_OUTPUT)
|
||||||
|
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
|
openApi()
|
||||||
|
redoc()
|
||||||
route("/test") {
|
route("/test") {
|
||||||
route("/{id}") {
|
route("/{id}") {
|
||||||
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
|
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
|
||||||
@ -100,6 +117,10 @@ fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Routing.openApi() {
|
||||||
route("/openapi.json") {
|
route("/openapi.json") {
|
||||||
get {
|
get {
|
||||||
call.respond(
|
call.respond(
|
||||||
@ -134,4 +155,41 @@ fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Routing.redoc() {
|
||||||
|
route("/docs") {
|
||||||
|
get {
|
||||||
|
call.respondHtml {
|
||||||
|
head {
|
||||||
|
title {
|
||||||
|
// TODO Make this load project title
|
||||||
|
+"Docs"
|
||||||
|
}
|
||||||
|
meta {
|
||||||
|
charset = "utf-8"
|
||||||
|
}
|
||||||
|
meta {
|
||||||
|
name = "viewport"
|
||||||
|
content = "width=device-width, initial-scale=1"
|
||||||
|
}
|
||||||
|
link {
|
||||||
|
href = "https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
|
||||||
|
rel = "stylesheet"
|
||||||
|
}
|
||||||
|
style {
|
||||||
|
unsafe {
|
||||||
|
raw("body { margin: 0; padding: 0; }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
// TODO Make this its own DSL class
|
||||||
|
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
|
||||||
|
script {
|
||||||
|
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user