lots of stuff, no time to s-plain (#5)

This commit is contained in:
Ryan Brink
2021-04-12 21:15:10 -04:00
committed by GitHub
parent 492933d728
commit a7505483c4
40 changed files with 410 additions and 287 deletions

View File

@ -6,6 +6,7 @@ plugins {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.bundles.ktor)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")

View File

@ -1,12 +1,128 @@
package org.leafygreens.kompendium
import org.leafygreens.kompendium.models.OpenApiSpec
import org.leafygreens.kompendium.models.OpenApiSpecInfo
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.createRouteFromPath
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineInterceptor
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.KompendiumInternal
import org.leafygreens.kompendium.models.oas.ArraySchema
import org.leafygreens.kompendium.models.oas.FormatSchema
import org.leafygreens.kompendium.models.oas.ObjectSchema
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponentSchema
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
import org.leafygreens.kompendium.models.oas.SimpleSchema
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.util.Helpers.calculatePath
import org.leafygreens.kompendium.util.Helpers.putPairIfAbsent
class Kompendium {
val spec = OpenApiSpec(
object Kompendium {
val openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
fun Route.notarizedGet(info: MethodInfo, body: PipelineInterceptor<Unit, ApplicationCall>): Route {
val path = calculatePath()
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.get = OpenApiSpecPathItemOperation(
summary = info.summary,
description = info.description,
tags = info.tags
)
return method(HttpMethod.Get) { handle(body) }
}
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> Route.notarizedPost(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = generateComponentSchemas<TQ, TP, TR>(info, body) { i, b ->
val path = calculatePath()
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.post = OpenApiSpecPathItemOperation(
summary = i.summary,
description = i.description,
tags = i.tags
)
return method(HttpMethod.Post) { handle(b) }
}
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> Route.notarizedPut(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = generateComponentSchemas<TQ, TP, TR>(info, body) { i, b ->
val path = calculatePath()
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.put = OpenApiSpecPathItemOperation(
summary = i.summary,
description = i.description,
tags = i.tags
)
return method(HttpMethod.Put) { handle(b) }
}
@OptIn(KompendiumInternal::class)
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> generateComponentSchemas(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
block: (MethodInfo, PipelineInterceptor<Unit, ApplicationCall>) -> Route
): Route {
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TQ::class))
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TR::class))
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TP::class))
return block.invoke(info, body)
}
@KompendiumInternal
// TODO Investigate a caching mechanism to reduce overhead... then just reference once created
fun objectSchemaPair(clazz: KClass<*>): Pair<String, ObjectSchema> {
val o = objectSchema(clazz)
return Pair(clazz.qualifiedName!!, o)
}
private fun objectSchema(clazz: KClass<*>): ObjectSchema =
ObjectSchema(properties = clazz.memberProperties.associate { prop ->
val field = prop.javaField?.type?.kotlin
val anny = prop.findAnnotation<KompendiumField>()
val schema = when (field) {
List::class -> listFieldSchema(prop)
else -> fieldToSchema(field as KClass<*>)
}
val name = anny?.let {
anny.name
} ?: prop.name
Pair(name, schema)
})
private fun listFieldSchema(prop: KProperty<*>): ArraySchema {
val listType = ((prop.javaField?.genericType
as ParameterizedType).actualTypeArguments.first()
as Class<*>).kotlin
return ArraySchema(fieldToSchema(listType))
}
@OptIn(KompendiumInternal::class)
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)
}
}

View File

@ -0,0 +1,5 @@
package org.leafygreens.kompendium.annotations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class KompendiumField(val name: String)

View File

@ -0,0 +1,18 @@
package org.leafygreens.kompendium.annotations
@Suppress("DEPRECATION")
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "This API internal to Kompendium and should not be used. It could be removed or changed without notice."
)
@Experimental(level = Experimental.Level.WARNING)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.PROPERTY_SETTER
)
annotation class KompendiumInternal

View File

@ -1,14 +0,0 @@
package org.leafygreens.kompendium.models
data class OpenApiSpec(
val openapi: String = "3.0.3",
val info: OpenApiSpecInfo? = null,
// TODO Needs to default to server object with url of `/`
val servers: MutableList<OpenApiSpecServer>? = null,
val paths: MutableMap<String, OpenApiSpecPathItem>? = null,
val components: OpenApiSpecComponents? = null,
// todo needs to reference objects in the components -> security scheme 🤔
val security: List<Map<String, List<String>>>? = null,
val tags: List<OpenApiSpecTag>? = null,
val externalDocs: OpenApiSpecExternalDocumentation? = null
)

View File

@ -1,6 +0,0 @@
package org.leafygreens.kompendium.models
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
data class OpenApiSpecComponents(
val securitySchemes: Map<String, OpenApiSpecSchema>
)

View File

@ -1,16 +0,0 @@
package org.leafygreens.kompendium.models
data class OpenApiSpecPathItem(
val summary: String? = null,
val description: String? = null,
val get: OpenApiSpecPathItemOperation? = null,
val put: OpenApiSpecPathItemOperation? = null,
val post: OpenApiSpecPathItemOperation? = null,
val delete: OpenApiSpecPathItemOperation? = null,
val options: OpenApiSpecPathItemOperation? = null,
val head: OpenApiSpecPathItemOperation? = null,
val patch: OpenApiSpecPathItemOperation? = null,
val trace: OpenApiSpecPathItemOperation? = null,
val servers: List<OpenApiSpecServer>? = null,
val parameters: List<OpenApiSpecReferencable>? = null
)

View File

@ -1,19 +0,0 @@
package org.leafygreens.kompendium.models
data class OpenApiSpecPathItemOperation(
val tags: Set<String> = emptySet(),
val summary: String? = null,
val description: String? = null,
val externalDocs: OpenApiSpecExternalDocumentation? = null,
val operationId: String? = null,
val parameters: List<OpenApiSpecReferencable>? = null,
val requestBody: OpenApiSpecReferencable? = null,
// TODO How to enforce `default` requirement 🧐
val responses: Map<String, OpenApiSpecReferencable>? = null,
val callbacks: Map<String, OpenApiSpecReferencable>? = null,
val deprecated: Boolean = false,
// todo big yikes... also needs to reference objects in the security scheme 🤔
val security: List<Map<String, List<String>>>? = null,
val servers: List<OpenApiSpecServer>? = null,
val `x-codegen-request-body-name`: String? = null
)

View File

@ -0,0 +1,3 @@
package org.leafygreens.kompendium.models.meta
data class MethodInfo(val summary: String, val description: String? = null, val tags: Set<String> = emptySet())

View File

@ -0,0 +1,14 @@
package org.leafygreens.kompendium.models.oas
data class OpenApiSpec(
val openapi: String = "3.0.3",
val info: OpenApiSpecInfo,
// TODO Needs to default to server object with url of `/`
val servers: MutableList<OpenApiSpecServer> = mutableListOf(),
val paths: MutableMap<String, OpenApiSpecPathItem> = mutableMapOf(),
val components: OpenApiSpecComponents = OpenApiSpecComponents(),
// todo needs to reference objects in the components -> security scheme 🤔
val security: MutableList<Map<String, List<String>>> = mutableListOf(),
val tags: MutableList<OpenApiSpecTag> = mutableListOf(),
val externalDocs: OpenApiSpecExternalDocumentation? = null
)

View File

@ -0,0 +1,14 @@
package org.leafygreens.kompendium.models.oas
// TODO Enum for type?
sealed class OpenApiSpecComponentSchema(open val type: String)
data class ObjectSchema(
val properties: Map<String, OpenApiSpecComponentSchema>
) : OpenApiSpecComponentSchema("object")
data class SimpleSchema(override val type: String) : OpenApiSpecComponentSchema(type)
data class FormatSchema(val format: String, override val type: String) : OpenApiSpecComponentSchema(type)
data class ArraySchema(val items: OpenApiSpecComponentSchema) : OpenApiSpecComponentSchema("array")

View File

@ -0,0 +1,7 @@
package org.leafygreens.kompendium.models.oas
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
data class OpenApiSpecComponents(
val schemas: MutableMap<String, OpenApiSpecComponentSchema> = mutableMapOf(),
val securitySchemes: MutableMap<String, OpenApiSpecSchema> = mutableMapOf()
)

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
import java.net.URI

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
import java.net.URI

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
import java.net.URI

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecLink(
val operationRef: String?, // todo mutually exclusive with operationId

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
// TODO Oof -> https://swagger.io/specification/#media-type-object
data class OpenApiSpecMediaType(

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
import java.net.URI

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecOAuthFlows(
val implicit: OpenApiSpecOAuthFlow?,

View File

@ -0,0 +1,14 @@
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecPathItem(
var get: OpenApiSpecPathItemOperation? = null,
var put: OpenApiSpecPathItemOperation? = null,
var post: OpenApiSpecPathItemOperation? = null,
var delete: OpenApiSpecPathItemOperation? = null,
var options: OpenApiSpecPathItemOperation? = null,
var head: OpenApiSpecPathItemOperation? = null,
var patch: OpenApiSpecPathItemOperation? = null,
var trace: OpenApiSpecPathItemOperation? = null,
var servers: List<OpenApiSpecServer>? = null,
var parameters: List<OpenApiSpecReferencable>? = null
)

View File

@ -0,0 +1,19 @@
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecPathItemOperation(
var tags: Set<String> = emptySet(),
var summary: String? = null,
var description: String? = null,
var externalDocs: OpenApiSpecExternalDocumentation? = null,
var operationId: String? = null,
var parameters: List<OpenApiSpecReferencable>? = null,
var requestBody: OpenApiSpecReferencable? = null,
// TODO How to enforce `default` requirement 🧐
var responses: Map<String, OpenApiSpecReferencable>? = null,
var callbacks: Map<String, OpenApiSpecReferencable>? = null,
var deprecated: Boolean = false,
// todo big yikes... also needs to reference objects in the security scheme 🤔
var security: List<Map<String, List<String>>>? = null,
var servers: List<OpenApiSpecServer>? = null,
var `x-codegen-request-body-name`: String? = null
)

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
sealed class OpenApiSpecReferencable
@ -9,10 +9,10 @@ data class OpenApiSpecCallback(
) : OpenApiSpecReferencable()
data class OpenApiSpecResponse(
val description: String? = null,
val headers: Map<String, OpenApiSpecReferencable>? = null,
val content: Map<String, OpenApiSpecMediaType>? = null,
val links: Map<String, OpenApiSpecReferencable>? = null
val description: String? = null,
val headers: Map<String, OpenApiSpecReferencable>? = null,
val content: Map<String, OpenApiSpecMediaType>? = null,
val links: Map<String, OpenApiSpecReferencable>? = null
) : OpenApiSpecReferencable()
data class OpenApiSpecHeader(
@ -34,7 +34,7 @@ data class OpenApiSpecParameter(
) : OpenApiSpecReferencable()
data class OpenApiSpecRequest(
val description: String?,
val content: Map<String, OpenApiSpecMediaType>,
val required: Boolean = false
val description: String?,
val content: Map<String, OpenApiSpecMediaType>,
val required: Boolean = false
) : OpenApiSpecReferencable()

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
sealed class OpenApiSpecSchema
@ -21,11 +21,11 @@ data class OpenApiSpecSchemaRef(
) : OpenApiSpecSchema()
data class OpenApiSpecSchemaSecurity(
val type: String? = null, // TODO Enum? "apiKey", "http", "oauth2", "openIdConnect"
val name: String? = null,
val `in`: String? = null,
val scheme: String? = null,
val flows: OpenApiSpecOAuthFlows? = null,
val bearerFormat: String? = null,
val description: String? = null,
val type: String? = null, // TODO Enum? "apiKey", "http", "oauth2", "openIdConnect"
val name: String? = null,
val `in`: String? = null,
val scheme: String? = null,
val flows: OpenApiSpecOAuthFlows? = null,
val bearerFormat: String? = null,
val description: String? = null,
) : OpenApiSpecSchema()

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
import java.net.URI

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecServerVariable(
val `enum`: Set<String>, // todo enforce not empty

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.models
package org.leafygreens.kompendium.models.oas
data class OpenApiSpecTag(
val name: String,

View File

@ -0,0 +1,21 @@
package org.leafygreens.kompendium.util
import io.ktor.routing.PathSegmentConstantRouteSelector
import io.ktor.routing.PathSegmentParameterRouteSelector
import io.ktor.routing.RootRouteSelector
import io.ktor.routing.Route
import io.ktor.util.InternalAPI
object Helpers {
@OptIn(InternalAPI::class)
fun Route.calculatePath(tail: String = ""): String = when (selector) {
is RootRouteSelector -> tail
is PathSegmentParameterRouteSelector -> parent?.calculatePath("/$selector$tail") ?: "/{$selector}$tail"
is PathSegmentConstantRouteSelector -> parent?.calculatePath("/$selector$tail") ?: "/$selector$tail"
else -> error("unknown selector type $selector")
}
fun <K, V> MutableMap<K, V>.putPairIfAbsent(pair: Pair<K, V>) = putIfAbsent(pair.first, pair.second)
}

View File

@ -7,8 +7,7 @@ internal class KompendiumTest {
@Test
fun `Kompendium can be instantiated with no details`() {
val kompendium = Kompendium()
assertEquals(kompendium.spec.openapi, "3.0.3", "Kompendium has a default spec version of 3.0.3")
assertEquals(Kompendium.openApiSpec.openapi, "3.0.3", "Kompendium has a default spec version of 3.0.3")
}
}

View File

@ -2,26 +2,26 @@ package org.leafygreens.kompendium.util
import java.io.File
import java.net.URI
import org.leafygreens.kompendium.models.OpenApiSpec
import org.leafygreens.kompendium.models.OpenApiSpecComponents
import org.leafygreens.kompendium.models.OpenApiSpecExternalDocumentation
import org.leafygreens.kompendium.models.OpenApiSpecInfo
import org.leafygreens.kompendium.models.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.OpenApiSpecParameter
import org.leafygreens.kompendium.models.OpenApiSpecPathItem
import org.leafygreens.kompendium.models.OpenApiSpecPathItemOperation
import org.leafygreens.kompendium.models.OpenApiSpecRequest
import org.leafygreens.kompendium.models.OpenApiSpecResponse
import org.leafygreens.kompendium.models.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.OpenApiSpecServer
import org.leafygreens.kompendium.models.OpenApiSpecTag
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponents
import org.leafygreens.kompendium.models.oas.OpenApiSpecExternalDocumentation
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.models.oas.OpenApiSpecTag
object TestData {
fun getFileSnapshot(fileName: String): String {
@ -61,7 +61,7 @@ object TestData {
url = URI("http://petstore.swagger.io/v2")
)
),
tags = listOf(
tags = mutableListOf(
OpenApiSpecTag(
name = "pet",
description = "Everything about your Pets",
@ -201,7 +201,7 @@ object TestData {
)
),
components = OpenApiSpecComponents(
securitySchemes = mapOf(
securitySchemes = mutableMapOf(
"petstore_auth" to OpenApiSpecSchemaSecurity(
type = "oauth2",
flows = OpenApiSpecOAuthFlows(
@ -219,7 +219,8 @@ object TestData {
name = "api_key",
`in` = "header"
)
)
),
schemas = mutableMapOf()
)
)
}

View File

@ -153,6 +153,7 @@
}
},
"components" : {
"schemas" : { },
"securitySchemes" : {
"petstore_auth" : {
"type" : "oauth2",
@ -173,6 +174,7 @@
}
}
},
"security" : [ ],
"tags" : [ {
"name" : "pet",
"description" : "Everything about your Pets",