feat: NotarizedResource plugin

This commit is contained in:
Geir Sagberg
2022-11-09 14:11:42 +01:00
committed by GitHub
parent 01b6b59cf5
commit 3c57c8f5e4
8 changed files with 350 additions and 44 deletions

View File

@ -0,0 +1,21 @@
package io.bkbn.kompendium.resources
import io.ktor.resources.Resource
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
fun KClass<*>.getResourcePathFromClass(): String {
val resource = findAnnotation<Resource>()
?: error("Cannot notarize a resource without annotating with @Resource")
val path = resource.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Resource>() }
return if (parent == null) {
path
} else {
parent.getResourcePathFromClass() + path
}
}

View File

@ -0,0 +1,35 @@
package io.bkbn.kompendium.resources
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.plugin.NotarizedRoute.addToSpec
import io.bkbn.kompendium.core.plugin.NotarizedRoute.calculateRoutePath
import io.bkbn.kompendium.core.plugin.NotarizedRoute.collectAuthMethods
import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.Hook
import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route
object NotarizedResource {
object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> {
override fun install(pipeline: ApplicationCallPipeline, handler: (ApplicationCallPipeline) -> Unit) {
handler(pipeline)
}
}
inline operator fun <reified T> invoke() = createRouteScopedPlugin(
name = "NotarizedResource<${T::class.qualifiedName}>",
createConfiguration = NotarizedRoute::Config
) {
on(InstallHook) {
val route = it as? Route ?: return@on
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val routePath = route.calculateRoutePath()
val authMethods = route.collectAuthMethods()
val resourcePath = T::class.getResourcePathFromClass()
val fullPath = "$routePath$resourcePath"
addToSpec(spec, fullPath, authMethods)
}
}
}

View File

@ -12,12 +12,8 @@ import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.resources.Resource
import io.ktor.server.application.createApplicationPlugin
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
object NotarizedResources {
@ -45,7 +41,7 @@ object NotarizedResources {
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
pluginConfig.resources.forEach { (k, v) ->
val resource = k.getResourcesFromClass()
val resource = k.getResourcePathFromClass()
val path = spec.paths[resource] ?: Path()
path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
v.get?.addToSpec(path, spec, v, serializableReader, resource)
@ -59,20 +55,4 @@ object NotarizedResources {
spec.paths[resource] = path
}
}
private fun KClass<*>.getResourcesFromClass(): String {
// todo if parent
val resource = findAnnotation<Resource>()
?: error("Cannot notarize a resource without annotating with @Resource")
val path = resource.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Resource>() }
return if (parent == null) {
path
} else {
parent.getResourcesFromClass() + path
}
}
}

View File

@ -14,9 +14,11 @@ import io.ktor.server.application.install
import io.ktor.server.resources.Resources
import io.ktor.server.resources.get
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
class KompendiumResourcesTest : DescribeSpec({
describe("Resource Tests") {
describe("NotarizedResources Tests") {
it("Can notarize a simple resource") {
openApiTestAllSerializers(
snapshotName = "T0001__simple_resource.json",
@ -117,4 +119,72 @@ class KompendiumResourcesTest : DescribeSpec({
}
}
}
describe("NotarizedResource Tests") {
it("Can notarize resources in route") {
openApiTestAllSerializers(
snapshotName = "T0003__resources_in_route.json",
applicationSetup = {
install(Resources)
}
) {
route("/api") {
typeEditDocumentation()
get<Type.Edit> { edit ->
call.respondText("Listing ${edit.parent.name}")
}
typeOtherDocumentation()
get<Type.Other> { other ->
call.respondText("Listing ${other.parent.name}, page ${other.page}")
}
}
}
}
}
})
private fun Route.typeOtherDocumentation() {
install(NotarizedResource<Type.Other>()) {
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
)
get = GetInfo.builder {
summary("Other")
description("example resource")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
}
}
private fun Route.typeEditDocumentation() {
install(NotarizedResource<Type.Edit>()) {
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary("Edit")
description("example resource")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
}
}

View File

@ -0,0 +1,124 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/api/type/{name}/edit": {
"get": {
"tags": [],
"summary": "Edit",
"description": "example resource",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
},
"/api/type/{name}/other/{page}": {
"get": {
"tags": [],
"summary": "Other",
"description": "example resource",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "page",
"in": "path",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}