feat: NotarizedResource plugin
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
124
resources/src/test/resources/T0003__resources_in_route.json
Normal file
124
resources/src/test/resources/T0003__resources_in_route.json
Normal 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": []
|
||||
}
|
Reference in New Issue
Block a user