document top level collectiom (#24)
This commit is contained in:
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.3.0] - April 17th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed response and request annotations in favor of MethodInfo extension.
|
||||||
|
- Modified notarization to add the correct reference slug regardless of type
|
||||||
|
|
||||||
## [0.2.0] - April 16th, 2021
|
## [0.2.0] - April 16th, 2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
72
README.md
72
README.md
@ -68,26 +68,39 @@ meaning that swapping in a default Ktor route and a Kompendium `notarized` route
|
|||||||
### Supplemental Annotations
|
### Supplemental Annotations
|
||||||
|
|
||||||
In general, Kompendium tries to limit the number of annotations that developers need to use in order to get an app
|
In general, Kompendium tries to limit the number of annotations that developers need to use in order to get an app
|
||||||
integrated. However, there are a couple areas that it made sense, at least for an MVP.
|
integrated.
|
||||||
|
|
||||||
Currently, there are three Kompendium annotations
|
Currently, there is only a single Kompendium annotation
|
||||||
|
|
||||||
- `KompendiumRequest`
|
|
||||||
- `KompendiumResponse`
|
|
||||||
- `KompendiumField`
|
- `KompendiumField`
|
||||||
|
|
||||||
These are aimed at offering modifications at the request, response, and field level respectively, and offer things such
|
The intended purpose is to offer field level overrides such as naming conventions (ie snake instead of camel).
|
||||||
as response status codes, field name overrides, and OpenApi metadata such as `description`.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
The full source code can be found in the `kompendium-playground` module. Here we show just the adjustments
|
||||||
|
needed to a standard Ktor server to get up and running in Kompendium.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Minimal API Example
|
// Minimal API Example
|
||||||
|
fun main() {
|
||||||
|
embeddedServer(
|
||||||
|
Netty,
|
||||||
|
port = 8081,
|
||||||
|
module = Application::mainModule
|
||||||
|
).start(wait = true)
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -109,52 +122,13 @@ fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
route("/openapi.json") {
|
|
||||||
get {
|
|
||||||
call.respond(openApiSpec.copy(
|
|
||||||
info = OpenApiSpecInfo(
|
|
||||||
title = "Test API",
|
|
||||||
version = "1.3.3.7",
|
|
||||||
description = "An amazing, fully-ish 😉 generated API spec"
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancillary Data
|
|
||||||
data class ExampleParams(val a: String, val aa: Int)
|
|
||||||
|
|
||||||
data class ExampleNested(val nesty: String)
|
|
||||||
|
|
||||||
@KompendiumResponse(status = KompendiumHttpCodes.NO_CONTENT, "Entity was deleted successfully")
|
|
||||||
object DeleteResponse
|
|
||||||
|
|
||||||
@KompendiumRequest("Example Request")
|
|
||||||
data class ExampleRequest(
|
|
||||||
@KompendiumField(name = "field_name")
|
|
||||||
val fieldName: ExampleNested,
|
|
||||||
val b: Double,
|
|
||||||
val aaa: List<Long>
|
|
||||||
)
|
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
|
||||||
data class ExampleResponse(val c: String)
|
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.CREATED, "Created Successfully")
|
|
||||||
data class ExampleCreatedResponse(val id: Int, val c: String)
|
|
||||||
|
|
||||||
object KompendiumTOC {
|
|
||||||
val testIdGetInfo = MethodInfo("Get Test", "Test for getting", tags = setOf("test", "example", "get"))
|
|
||||||
val testSingleGetInfo = MethodInfo("Another get test", "testing more")
|
|
||||||
val testSinglePostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
|
||||||
val testSinglePutInfo = MethodInfo("Test put endpoint", "Put your tests here!")
|
|
||||||
val testSingleDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes")
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This example would output the following json spec https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
|
When run in the playground, this would output the following at `/openapi.json`
|
||||||
|
|
||||||
|
https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=0.2.0
|
project.version=0.3.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -5,11 +5,12 @@ import io.ktor.http.HttpMethod
|
|||||||
import io.ktor.routing.Route
|
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.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
import org.leafygreens.kompendium.Kontent.generateKontent
|
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.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.OpenApiSpec
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
|
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.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.getReferenceSlug
|
||||||
|
|
||||||
object Kompendium {
|
object Kompendium {
|
||||||
|
|
||||||
@ -29,94 +30,102 @@ object Kompendium {
|
|||||||
paths = mutableMapOf()
|
paths = mutableMapOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
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<Unit, TResp>() {
|
): Route = notarizationPreFlight<Unit, TResp>() { requestType, responseType ->
|
||||||
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(HttpMethod.Get, requestType, responseType)
|
||||||
return method(HttpMethod.Get) { handle(body) }
|
return method(HttpMethod.Get) { handle(body) }
|
||||||
}
|
}
|
||||||
|
|
||||||
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<TReq, TResp>() {
|
): Route = notarizationPreFlight<TReq, TResp>() { requestType, responseType ->
|
||||||
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(HttpMethod.Post, requestType, responseType)
|
||||||
return method(HttpMethod.Post) { handle(body) }
|
return method(HttpMethod.Post) { handle(body) }
|
||||||
}
|
}
|
||||||
|
|
||||||
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<TReq, TResp>() {
|
): Route = notarizationPreFlight<TReq, TResp>() { requestType, responseType ->
|
||||||
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(HttpMethod.Put, requestType, responseType)
|
||||||
return method(HttpMethod.Put) { handle(body) }
|
return method(HttpMethod.Put) { handle(body) }
|
||||||
}
|
}
|
||||||
|
|
||||||
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<Unit, TResp> {
|
): Route = notarizationPreFlight<Unit, TResp> { requestType, responseType ->
|
||||||
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(HttpMethod.Delete, requestType, responseType)
|
||||||
return method(HttpMethod.Delete) { handle(body) }
|
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,
|
summary = this.summary,
|
||||||
description = this.description,
|
description = this.description,
|
||||||
tags = this.tags,
|
tags = this.tags,
|
||||||
deprecated = this.deprecated,
|
deprecated = this.deprecated,
|
||||||
responses = parseResponseAnnotation<TResp>()?.let { mapOf(it) },
|
responses = responseType.toSpec(responseInfo)?.let { mapOf(it) },
|
||||||
requestBody = parseRequestAnnotation<TReq>()
|
requestBody = if (method != HttpMethod.Get) requestType.toSpec(requestInfo) else null
|
||||||
)
|
)
|
||||||
|
|
||||||
inline fun <reified TReq : Any, reified TResp : Any> generateComponentSchemas(
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
block: () -> Route
|
inline fun <reified TReq : Any, reified TResp : Any> notarizationPreFlight(
|
||||||
|
block: (KType, KType) -> Route
|
||||||
): Route {
|
): Route {
|
||||||
val responseKontent = generateKontent<TResp>()
|
val responseKontent = generateKontent<TResp>()
|
||||||
val requestKontent = generateKontent<TReq>()
|
val requestKontent = generateKontent<TReq>()
|
||||||
openApiSpec.components.schemas.putAll(responseKontent)
|
openApiSpec.components.schemas.putAll(responseKontent)
|
||||||
openApiSpec.components.schemas.putAll(requestKontent)
|
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
|
Unit::class -> null
|
||||||
else -> when (val anny = TReq::class.findAnnotation<KompendiumRequest>()) {
|
else -> when (requestInfo) {
|
||||||
null -> null
|
null -> null
|
||||||
else -> OpenApiSpecRequest(
|
else -> OpenApiSpecRequest(
|
||||||
description = anny.description,
|
description = requestInfo.description,
|
||||||
content = anny.mediaTypes.associate {
|
content = requestInfo.mediaTypes.associateWith {
|
||||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TReq::class.simpleName}")
|
val ref = getReferenceSlug()
|
||||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
|
||||||
Pair(it, mediaType)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified TResp> parseResponseAnnotation(): Pair<Int, OpenApiSpecResponse>? = when (TResp::class) {
|
private fun KType.toSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (this) {
|
||||||
Unit::class -> null
|
Unit::class -> null // TODO Maybe not though? could be unit but 200 🤔
|
||||||
else -> when (val anny = TResp::class.findAnnotation<KompendiumResponse>()) {
|
else -> when (responseInfo) {
|
||||||
null -> null
|
null -> null // TODO again probably revisit this
|
||||||
else -> {
|
else -> {
|
||||||
val specResponse = OpenApiSpecResponse(
|
val specResponse = OpenApiSpecResponse(
|
||||||
description = anny.description,
|
description = responseInfo.description,
|
||||||
content = anny.mediaTypes.associate {
|
content = responseInfo.mediaTypes.associateWith {
|
||||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TResp::class.simpleName}")
|
val ref = getReferenceSlug()
|
||||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
|
||||||
Pair(it, mediaType)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Pair(anny.status, specResponse)
|
Pair(responseInfo.status, specResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ object Kontent {
|
|||||||
val referenceName = genericNameAdapter(type, clazz)
|
val referenceName = genericNameAdapter(type, clazz)
|
||||||
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
||||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||||
val updatedCache = generateKTypeKontent(valType!!, cache)
|
val updatedCache = generateKTypeKontent(valType, cache)
|
||||||
return updatedCache.plus(referenceName to schema)
|
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
|
package org.leafygreens.kompendium.models.meta
|
||||||
|
|
||||||
|
// TODO Seal and extend by method type?
|
||||||
data class MethodInfo(
|
data class MethodInfo(
|
||||||
val summary: String,
|
val summary: String,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
|
val responseInfo: ResponseInfo? = null,
|
||||||
|
val requestInfo: RequestInfo? = null,
|
||||||
val tags: Set<String> = emptySet(),
|
val tags: Set<String> = emptySet(),
|
||||||
val deprecated: Boolean = false
|
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
|
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
|
* Will build a reference slug that is useful for schema caching and references, particularly
|
||||||
* in the case of a class with type parameters
|
* 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.notarizedPost
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
||||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
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.OpenApiSpecInfo
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
||||||
import org.leafygreens.kompendium.util.ComplexRequest
|
import org.leafygreens.kompendium.util.ComplexRequest
|
||||||
|
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||||
import org.leafygreens.kompendium.util.TestCreatedResponse
|
import org.leafygreens.kompendium.util.TestCreatedResponse
|
||||||
import org.leafygreens.kompendium.util.TestData
|
import org.leafygreens.kompendium.util.TestData
|
||||||
import org.leafygreens.kompendium.util.TestDeleteResponse
|
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 {
|
private companion object {
|
||||||
val testGetInfo = MethodInfo("Another get test", "testing more")
|
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
||||||
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
|
||||||
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!")
|
val testDeleteResponse = ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor")
|
||||||
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes")
|
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() {
|
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() {
|
private fun Application.complexType() {
|
||||||
routing {
|
routing {
|
||||||
route("/test") {
|
route("/test") {
|
||||||
|
@ -2,8 +2,6 @@ package org.leafygreens.kompendium.util
|
|||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
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)
|
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)
|
data class TestWithUUID(val id: UUID)
|
||||||
|
|
||||||
@KompendiumRequest("Example Request")
|
|
||||||
data class TestRequest(
|
data class TestRequest(
|
||||||
@KompendiumField(name = "field_name")
|
@KompendiumField(name = "field_name")
|
||||||
val fieldName: TestNested,
|
val fieldName: TestNested,
|
||||||
@ -33,16 +30,12 @@ data class TestRequest(
|
|||||||
val aaa: List<Long>
|
val aaa: List<Long>
|
||||||
)
|
)
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
|
||||||
data class TestResponse(val c: String)
|
data class TestResponse(val c: String)
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.CREATED, "Created Successfully")
|
|
||||||
data class TestCreatedResponse(val id: Int, val c: String)
|
data class TestCreatedResponse(val id: Int, val c: String)
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.NO_CONTENT, "Entity was deleted successfully")
|
|
||||||
object TestDeleteResponse
|
object TestDeleteResponse
|
||||||
|
|
||||||
@KompendiumRequest("Request object to create a backbone project")
|
|
||||||
data class ComplexRequest(
|
data class ComplexRequest(
|
||||||
val org: String,
|
val org: String,
|
||||||
@KompendiumField("amazing_field")
|
@KompendiumField("amazing_field")
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"summary" : "Test put endpoint",
|
"summary" : "Test put endpoint",
|
||||||
"description" : "Put your tests here!",
|
"description" : "Put your tests here!",
|
||||||
"requestBody" : {
|
"requestBody" : {
|
||||||
"description" : "Request object to create a backbone project",
|
"description" : "A Test request",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"schema" : {
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"required" : false
|
"required" : false
|
||||||
},
|
},
|
||||||
"responses" : {
|
"responses" : {
|
||||||
"200" : {
|
"201" : {
|
||||||
"description" : "A Successful Endeavor",
|
"description" : "A Successful Endeavor",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"description" : "testing my deletes",
|
"description" : "testing my deletes",
|
||||||
"responses" : {
|
"responses" : {
|
||||||
"204" : {
|
"204" : {
|
||||||
"description" : "Entity was deleted successfully",
|
"description" : "A Successful Endeavor",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"schema" : {
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"summary" : "Test post endpoint",
|
"summary" : "Test post endpoint",
|
||||||
"description" : "Post your tests here!",
|
"description" : "Post your tests here!",
|
||||||
"requestBody" : {
|
"requestBody" : {
|
||||||
"description" : "Example Request",
|
"description" : "A Test request",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"schema" : {
|
||||||
@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"responses" : {
|
"responses" : {
|
||||||
"201" : {
|
"201" : {
|
||||||
"description" : "Created Successfully",
|
"description" : "A Successful Endeavor",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"schema" : {
|
||||||
|
@ -28,6 +28,29 @@
|
|||||||
"tags" : [ ],
|
"tags" : [ ],
|
||||||
"summary" : "Test put endpoint",
|
"summary" : "Test put endpoint",
|
||||||
"description" : "Put your tests here!",
|
"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
|
"deprecated" : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"summary" : "Test put endpoint",
|
"summary" : "Test put endpoint",
|
||||||
"description" : "Put your tests here!",
|
"description" : "Put your tests here!",
|
||||||
"requestBody" : {
|
"requestBody" : {
|
||||||
"description" : "Example Request",
|
"description" : "A Test request",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"schema" : {
|
||||||
@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"responses" : {
|
"responses" : {
|
||||||
"201" : {
|
"201" : {
|
||||||
"description" : "Created Successfully",
|
"description" : "A Successful Endeavor",
|
||||||
"content" : {
|
"content" : {
|
||||||
"application/json" : {
|
"application/json" : {
|
||||||
"schema" : {
|
"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" : [ ]
|
||||||
|
}
|
@ -7,7 +7,6 @@ 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.html.respondHtml
|
||||||
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
|
||||||
@ -32,9 +31,9 @@ import org.leafygreens.kompendium.Kompendium.notarizedPost
|
|||||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
||||||
import org.leafygreens.kompendium.Kompendium.openApiSpec
|
import org.leafygreens.kompendium.Kompendium.openApiSpec
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||||
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.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.OpenApiSpecInfo
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
||||||
@ -54,38 +53,6 @@ fun main() {
|
|||||||
).start(wait = true)
|
).start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExampleParams(val a: String, val aa: Int)
|
|
||||||
|
|
||||||
data class ExampleNested(val nesty: String)
|
|
||||||
|
|
||||||
@KompendiumResponse(KompendiumHttpCodes.NO_CONTENT, "Entity was deleted successfully")
|
|
||||||
object DeleteResponse
|
|
||||||
|
|
||||||
@KompendiumRequest("Example Request")
|
|
||||||
data class ExampleRequest(
|
|
||||||
@KompendiumField(name = "field_name")
|
|
||||||
val fieldName: ExampleNested,
|
|
||||||
val b: Double,
|
|
||||||
val aaa: List<Long>
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val HTTP_OK = 200
|
|
||||||
private const val HTTP_CREATED = 201
|
|
||||||
|
|
||||||
@KompendiumResponse(HTTP_OK, "A Successful Endeavor")
|
|
||||||
data class ExampleResponse(val c: String)
|
|
||||||
|
|
||||||
@KompendiumResponse(HTTP_CREATED, "Created Successfully")
|
|
||||||
data class ExampleCreatedResponse(val id: Int, val c: String)
|
|
||||||
|
|
||||||
object KompendiumTOC {
|
|
||||||
val testIdGetInfo = MethodInfo("Get Test", "Test for getting", tags = setOf("test", "example", "get"))
|
|
||||||
val testSingleGetInfo = MethodInfo("Another get test", "testing more")
|
|
||||||
val testSinglePostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
|
||||||
val testSinglePutInfo = MethodInfo("Test put endpoint", "Put your tests here!")
|
|
||||||
val testSingleDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Application.mainModule() {
|
fun Application.mainModule() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
jackson {
|
jackson {
|
||||||
@ -120,6 +87,74 @@ fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ExampleParams(val a: String, val aa: Int)
|
||||||
|
|
||||||
|
data class ExampleNested(val nesty: String)
|
||||||
|
|
||||||
|
object DeleteResponse
|
||||||
|
|
||||||
|
data class ExampleRequest(
|
||||||
|
@KompendiumField(name = "field_name")
|
||||||
|
val fieldName: ExampleNested,
|
||||||
|
val b: Double,
|
||||||
|
val aaa: List<Long>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ExampleResponse(val c: String)
|
||||||
|
|
||||||
|
data class ExampleCreatedResponse(val id: Int, val c: String)
|
||||||
|
|
||||||
|
object KompendiumTOC {
|
||||||
|
val testIdGetInfo = MethodInfo(
|
||||||
|
summary = "Get Test",
|
||||||
|
description = "Test for the getting",
|
||||||
|
tags = setOf("test", "sample", "get"),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.OK,
|
||||||
|
description = "Returns sample info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testSingleGetInfo = MethodInfo(
|
||||||
|
summary = "Another get test",
|
||||||
|
description = "testing more",
|
||||||
|
tags = setOf("anotherTest", "sample"),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.OK,
|
||||||
|
description = "Returns a different sample"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testSinglePostInfo = MethodInfo(
|
||||||
|
summary = "Test post endpoint",
|
||||||
|
description = "Post your tests here!",
|
||||||
|
requestInfo = RequestInfo(
|
||||||
|
description = "Simple request body"
|
||||||
|
),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.CREATED,
|
||||||
|
description = "Worlds most complex response"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testSinglePutInfo = MethodInfo(
|
||||||
|
summary = "Test put endpoint",
|
||||||
|
description = "Put your tests here!",
|
||||||
|
requestInfo = RequestInfo(
|
||||||
|
description = "Info needed to perform this put request"
|
||||||
|
),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.CREATED,
|
||||||
|
description = "What we give you when u do the puts"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testSingleDeleteInfo = MethodInfo(
|
||||||
|
summary = "Test delete endpoint",
|
||||||
|
description = "testing my deletes",
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.NO_CONTENT,
|
||||||
|
description = "Signifies that your item was deleted succesfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun Routing.openApi() {
|
fun Routing.openApi() {
|
||||||
route("/openapi.json") {
|
route("/openapi.json") {
|
||||||
get {
|
get {
|
||||||
@ -162,8 +197,7 @@ fun Routing.redoc() {
|
|||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
// TODO Make this load project title
|
+"${openApiSpec.info.title}"
|
||||||
+"Docs"
|
|
||||||
}
|
}
|
||||||
meta {
|
meta {
|
||||||
charset = "utf-8"
|
charset = "utf-8"
|
||||||
@ -183,7 +217,7 @@ fun Routing.redoc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
// TODO Make this its own DSL class
|
// TODO needs to mirror openApi route
|
||||||
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
|
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
|
||||||
script {
|
script {
|
||||||
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
|
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
|
||||||
|
Reference in New Issue
Block a user