document top level collectiom (#24)

This commit is contained in:
Ryan Brink
2021-04-17 08:32:43 -04:00
committed by GitHub
parent 810f290f0d
commit 81e24f96dc
20 changed files with 308 additions and 158 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
} }
} }
} }

View File

@ -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)
} }

View File

@ -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"]
)

View File

@ -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"]
)

View File

@ -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
) )

View File

@ -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")
)

View File

@ -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")
)

View File

@ -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

View File

@ -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") {

View File

@ -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")

View File

@ -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" : {

View File

@ -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" : {

View File

@ -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" : {

View File

@ -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
} }
} }

View File

@ -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" : {

View 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" : [ ]
}

View File

@ -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"