document top level collectiom (#24)
This commit is contained in:
@ -5,11 +5,12 @@ import io.ktor.http.HttpMethod
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.method
|
||||
import io.ktor.util.pipeline.PipelineInterceptor
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
import org.leafygreens.kompendium.Kontent.generateKontent
|
||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.models.meta.RequestInfo
|
||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpec
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
|
||||
@ -18,8 +19,8 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
|
||||
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.getReferenceSlug
|
||||
|
||||
object Kompendium {
|
||||
|
||||
@ -29,94 +30,102 @@ object Kompendium {
|
||||
paths = mutableMapOf()
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<Unit, TResp>() {
|
||||
): Route = notarizationPreFlight<Unit, TResp>() { requestType, responseType ->
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.get = info.parseMethodInfo<Unit, TResp>()
|
||||
openApiSpec.paths[path]?.get = info.parseMethodInfo(HttpMethod.Get, requestType, responseType)
|
||||
return method(HttpMethod.Get) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<TReq, TResp>() {
|
||||
): Route = notarizationPreFlight<TReq, TResp>() { requestType, responseType ->
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.post = info.parseMethodInfo<TReq, TResp>()
|
||||
openApiSpec.paths[path]?.post = info.parseMethodInfo(HttpMethod.Post, requestType, responseType)
|
||||
return method(HttpMethod.Post) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
): Route = generateComponentSchemas<TReq, TResp>() {
|
||||
): Route = notarizationPreFlight<TReq, TResp>() { requestType, responseType ->
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.put = info.parseMethodInfo<TReq, TResp>()
|
||||
openApiSpec.paths[path]?.put = info.parseMethodInfo(HttpMethod.Put, requestType, responseType)
|
||||
return method(HttpMethod.Put) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<Unit, TResp> {
|
||||
): Route = notarizationPreFlight<Unit, TResp> { requestType, responseType ->
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.delete = info.parseMethodInfo<Unit, TResp>()
|
||||
openApiSpec.paths[path]?.delete = info.parseMethodInfo(HttpMethod.Delete, requestType, responseType)
|
||||
return method(HttpMethod.Delete) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TReq, reified TResp> MethodInfo.parseMethodInfo() = OpenApiSpecPathItemOperation(
|
||||
// TODO here down is a mess, needs refactor once core functionality is in place
|
||||
fun MethodInfo.parseMethodInfo(
|
||||
method: HttpMethod,
|
||||
requestType: KType,
|
||||
responseType: KType
|
||||
) = OpenApiSpecPathItemOperation(
|
||||
summary = this.summary,
|
||||
description = this.description,
|
||||
tags = this.tags,
|
||||
deprecated = this.deprecated,
|
||||
responses = parseResponseAnnotation<TResp>()?.let { mapOf(it) },
|
||||
requestBody = parseRequestAnnotation<TReq>()
|
||||
responses = responseType.toSpec(responseInfo)?.let { mapOf(it) },
|
||||
requestBody = if (method != HttpMethod.Get) requestType.toSpec(requestInfo) else null
|
||||
)
|
||||
|
||||
inline fun <reified TReq : Any, reified TResp : Any> generateComponentSchemas(
|
||||
block: () -> Route
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified TReq : Any, reified TResp : Any> notarizationPreFlight(
|
||||
block: (KType, KType) -> Route
|
||||
): Route {
|
||||
val responseKontent = generateKontent<TResp>()
|
||||
val requestKontent = generateKontent<TReq>()
|
||||
openApiSpec.components.schemas.putAll(responseKontent)
|
||||
openApiSpec.components.schemas.putAll(requestKontent)
|
||||
return block.invoke()
|
||||
val requestType = typeOf<TReq>()
|
||||
val responseType = typeOf<TResp>()
|
||||
return block.invoke(requestType, responseType)
|
||||
}
|
||||
|
||||
inline fun <reified TReq> parseRequestAnnotation(): OpenApiSpecRequest? = when (TReq::class) {
|
||||
// TODO These two lookin' real similar 👀 Combine?
|
||||
private fun KType.toSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (this) {
|
||||
Unit::class -> null
|
||||
else -> when (val anny = TReq::class.findAnnotation<KompendiumRequest>()) {
|
||||
else -> when (requestInfo) {
|
||||
null -> null
|
||||
else -> OpenApiSpecRequest(
|
||||
description = anny.description,
|
||||
content = anny.mediaTypes.associate {
|
||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TReq::class.simpleName}")
|
||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
||||
Pair(it, mediaType)
|
||||
description = requestInfo.description,
|
||||
content = requestInfo.mediaTypes.associateWith {
|
||||
val ref = getReferenceSlug()
|
||||
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified TResp> parseResponseAnnotation(): Pair<Int, OpenApiSpecResponse>? = when (TResp::class) {
|
||||
Unit::class -> null
|
||||
else -> when (val anny = TResp::class.findAnnotation<KompendiumResponse>()) {
|
||||
null -> null
|
||||
private fun KType.toSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (this) {
|
||||
Unit::class -> null // TODO Maybe not though? could be unit but 200 🤔
|
||||
else -> when (responseInfo) {
|
||||
null -> null // TODO again probably revisit this
|
||||
else -> {
|
||||
val specResponse = OpenApiSpecResponse(
|
||||
description = anny.description,
|
||||
content = anny.mediaTypes.associate {
|
||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TResp::class.simpleName}")
|
||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
||||
Pair(it, mediaType)
|
||||
description = responseInfo.description,
|
||||
content = responseInfo.mediaTypes.associateWith {
|
||||
val ref = getReferenceSlug()
|
||||
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
|
||||
}
|
||||
)
|
||||
Pair(anny.status, specResponse)
|
||||
Pair(responseInfo.status, specResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ object Kontent {
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||
val updatedCache = generateKTypeKontent(valType!!, cache)
|
||||
val updatedCache = generateKTypeKontent(valType, cache)
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
annotation class KompendiumRequest(
|
||||
val description: String,
|
||||
val required: Boolean = true,
|
||||
val mediaTypes: Array<String> = ["application/json"]
|
||||
)
|
@ -1,9 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class KompendiumResponse(
|
||||
val status: Int,
|
||||
val description: String,
|
||||
val mediaTypes: Array<String> = ["application/json"]
|
||||
)
|
@ -1,8 +1,11 @@
|
||||
package org.leafygreens.kompendium.models.meta
|
||||
|
||||
// TODO Seal and extend by method type?
|
||||
data class MethodInfo(
|
||||
val summary: String,
|
||||
val description: String? = null,
|
||||
val responseInfo: ResponseInfo? = null,
|
||||
val requestInfo: RequestInfo? = null,
|
||||
val tags: Set<String> = emptySet(),
|
||||
val deprecated: Boolean = false
|
||||
)
|
||||
|
@ -0,0 +1,7 @@
|
||||
package org.leafygreens.kompendium.models.meta
|
||||
|
||||
data class RequestInfo(
|
||||
val description: String,
|
||||
val required: Boolean = true,
|
||||
val mediaTypes: List<String> = listOf("application/json")
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package org.leafygreens.kompendium.models.meta
|
||||
|
||||
data class ResponseInfo(
|
||||
val status: Int, // TODO How to handle error codes?
|
||||
val description: String,
|
||||
val mediaTypes: List<String> = listOf("application/json")
|
||||
)
|
@ -92,6 +92,11 @@ object Helpers {
|
||||
return result
|
||||
}
|
||||
|
||||
fun KType.getReferenceSlug(): String = when {
|
||||
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
|
||||
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Will build a reference slug that is useful for schema caching and references, particularly
|
||||
* in the case of a class with type parameters
|
||||
|
@ -25,11 +25,14 @@ import org.leafygreens.kompendium.Kompendium.notarizedGet
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.models.meta.RequestInfo
|
||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
||||
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.OpenApiSpecServer
|
||||
import org.leafygreens.kompendium.util.ComplexRequest
|
||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||
import org.leafygreens.kompendium.util.TestCreatedResponse
|
||||
import org.leafygreens.kompendium.util.TestData
|
||||
import org.leafygreens.kompendium.util.TestDeleteResponse
|
||||
@ -289,11 +292,31 @@ internal class KompendiumTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can notarize a top level list response`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
returnsList()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("response_list.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val testGetInfo = MethodInfo("Another get test", "testing more")
|
||||
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
||||
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!")
|
||||
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes")
|
||||
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
||||
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
|
||||
val testDeleteResponse = ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor")
|
||||
val testRequest = RequestInfo("A Test request")
|
||||
val testGetInfo = MethodInfo("Another get test", "testing more", testGetResponse)
|
||||
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!", testPostResponse, testRequest)
|
||||
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!", testPostResponse, testRequest)
|
||||
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes", testDeleteResponse)
|
||||
}
|
||||
|
||||
private fun Application.configModule() {
|
||||
@ -387,6 +410,16 @@ internal class KompendiumTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.returnsList() {
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedGet<TestParams, List<TestResponse>>(testGetInfo) {
|
||||
call.respondText { "hey dude ur doing amazing work!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.complexType() {
|
||||
routing {
|
||||
route("/test") {
|
||||
|
@ -2,8 +2,6 @@ package org.leafygreens.kompendium.util
|
||||
|
||||
import java.util.UUID
|
||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||
|
||||
data class TestSimpleModel(val a: String, val b: Int)
|
||||
|
||||
@ -25,7 +23,6 @@ data class TestNested(val nesty: String)
|
||||
|
||||
data class TestWithUUID(val id: UUID)
|
||||
|
||||
@KompendiumRequest("Example Request")
|
||||
data class TestRequest(
|
||||
@KompendiumField(name = "field_name")
|
||||
val fieldName: TestNested,
|
||||
@ -33,16 +30,12 @@ data class TestRequest(
|
||||
val aaa: List<Long>
|
||||
)
|
||||
|
||||
@KompendiumResponse(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
||||
data class TestResponse(val c: String)
|
||||
|
||||
@KompendiumResponse(KompendiumHttpCodes.CREATED, "Created Successfully")
|
||||
data class TestCreatedResponse(val id: Int, val c: String)
|
||||
|
||||
@KompendiumResponse(KompendiumHttpCodes.NO_CONTENT, "Entity was deleted successfully")
|
||||
object TestDeleteResponse
|
||||
|
||||
@KompendiumRequest("Request object to create a backbone project")
|
||||
data class ComplexRequest(
|
||||
val org: String,
|
||||
@KompendiumField("amazing_field")
|
||||
|
@ -29,7 +29,7 @@
|
||||
"summary" : "Test put endpoint",
|
||||
"description" : "Put your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "Request object to create a backbone project",
|
||||
"description" : "A Test request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
@ -40,7 +40,7 @@
|
||||
"required" : false
|
||||
},
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"201" : {
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
|
@ -30,7 +30,7 @@
|
||||
"description" : "testing my deletes",
|
||||
"responses" : {
|
||||
"204" : {
|
||||
"description" : "Entity was deleted successfully",
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
|
@ -29,7 +29,7 @@
|
||||
"summary" : "Test post endpoint",
|
||||
"description" : "Post your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "Example Request",
|
||||
"description" : "A Test request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
@ -41,7 +41,7 @@
|
||||
},
|
||||
"responses" : {
|
||||
"201" : {
|
||||
"description" : "Created Successfully",
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
|
@ -28,6 +28,29 @@
|
||||
"tags" : [ ],
|
||||
"summary" : "Test put endpoint",
|
||||
"description" : "Put your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "A Test request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/Int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : false
|
||||
},
|
||||
"responses" : {
|
||||
"201" : {
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/Boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
"summary" : "Test put endpoint",
|
||||
"description" : "Put your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "Example Request",
|
||||
"description" : "A Test request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
@ -41,7 +41,7 @@
|
||||
},
|
||||
"responses" : {
|
||||
"201" : {
|
||||
"description" : "Created Successfully",
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
|
71
kompendium-core/src/test/resources/response_list.json
Normal file
71
kompendium-core/src/test/resources/response_list.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"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" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Another get test",
|
||||
"description" : "testing more",
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/List-TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"TestResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"List-TestResponse" : {
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/TestResponse"
|
||||
},
|
||||
"type" : "array"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
Reference in New Issue
Block a user