Feature/examplebodies (#44)

This commit is contained in:
Ryan Brink
2021-05-04 14:19:29 -04:00
committed by GitHub
parent ad33832ed3
commit 6b87519a64
23 changed files with 582 additions and 523 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## [0.8.0] - May 4th, 2021
### Added
- Support for example request and response bodies. Parameter examples / defaults are a separate issue for later.
### Changed
- Converted `MethodInfo` into a sealed class with distinct method types for Get, Post, Put, and Delete
## [0.7.0] - April 29th, 2021
### Added
@ -10,6 +20,7 @@
### Changed
- Refactored `kompendium-core` to break up the `Kompendium` object into slightly more manageable chunks
- Notarization Parameters can now be inferred from method info
## [0.6.2] - April 23rd, 2021

134
README.md
View File

@ -91,73 +91,45 @@ that a notarized route needs to analyze.
## 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.
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
```kotlin
// Minimal API Example
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(exceptionResponseInfo) {
notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
KompendiumHttpCodes.BAD_REQUEST,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
routing {
openApi()
redoc()
route("/test") {
route("/{id}") {
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
call.respondText("get by id")
}
}
route("/single") {
notarizedGet<ExampleParams, ExampleResponse>(testSingleGetInfo) {
call.respondText("get single")
}
notarizedPost<Unit, ExampleRequest, ExampleCreatedResponse>(testSinglePostInfo) {
call.respondText("test post")
}
notarizedPut<ExampleParams, ExampleRequest, ExampleCreatedResponse>(testSinglePutInfo) {
call.respondText { "hey" }
}
notarizedDelete<Unit, Unit>(testSingleDeleteInfo) {
call.respondText { "heya" }
}
}
route("/error") {
notarizedGet<Unit, ExampleResponse>(testSingleGetInfoWithThrowable) {
error("bad things just happened")
}
openApi(oas)
redoc(oas)
swaggerUI()
route("/potato/spud") {
notarizedGet(simpleGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class) // Must match an exception that has been notarized in the `StatusPages`
val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
responseInfo = ResponseInfo(
status = 200,
description = "nice",
examples = mapOf("test" to ExampleResponse(c = "spud"))
),
canThrow = setOf(Exception::class)
)
```
When run in the playground, this would output the following at `/openapi.json`
https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
### Kompendium Auth and security schemes
There is a separate library to handle security schemes: `kompendium-auth`.
@ -167,42 +139,40 @@ At the moment, the basic and jwt authentication is only supported.
A minimal example would be:
```kotlin
install(Authentication) {
notarizedBasic("basic") {
realm = "Ktor realm 1"
// configure basic authentication provider..
}
notarizedJwt("jwt") {
realm = "Ktor realm 2"
// configure jwt authentication provider...
}
install(Authentication) {
notarizedBasic("basic") {
realm = "Ktor realm 1"
// configure basic authentication provider..
}
routing {
authenticate("basic") {
route("/basic_auth") {
notarizedGet<TestParams, TestResponse>(
MethodInfo(
// securitySchemes needs to be set
"Another get test", "testing more", testGetResponse, securitySchemes = setOf("basic")
)
) {
call.respondText { "basic auth" }
}
}
}
authenticate("jwt") {
route("/jwt") {
notarizedGet<TestParams, TestResponse>(
MethodInfo(
// securitySchemes needs to be set
"Another get test", "testing more", testGetResponse, securitySchemes = setOf("jwt")
)
) {
call.respondText { "jwt" }
}
notarizedJwt("jwt") {
realm = "Ktor realm 2"
// configure jwt authentication provider...
}
}
routing {
authenticate("basic") {
route("/basic_auth") {
notarizedGet(basicAuthGetInfo) {
call.respondText { "basic auth" }
}
}
}
authenticate("jwt") {
route("/jwt") {
notarizedGet(jwtAuthGetInfo) {
call.respondText { "jwt" }
}
}
}
}
val basicAuthGetInfo = MethodInfo<Unit, ExampleResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse,
securitySchemes = setOf("basic")
)
val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt"))
```
### Enabling Swagger ui

View File

@ -81,8 +81,8 @@ complexity:
threshold: 80
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
functionThreshold: 10
constructorThreshold: 10
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotated: []

View File

@ -1,7 +1,7 @@
# Kompendium
project.version=0.7.0
project.version=0.8.0
# Kotlin
kotlin.code.style=official
# Gradle
#org.gradle.vfs.watch=true
#org.gradle.vfs.verbose=true
org.gradle.vfs.watch=true
org.gradle.vfs.verbose=true

View File

@ -2,16 +2,26 @@ package org.leafygreens.kompendium.auth
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.jackson.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.UserIdPrincipal
import io.ktor.auth.authenticate
import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.jackson.jackson
import io.ktor.response.respondText
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import kotlin.test.AfterTest
import kotlin.test.assertEquals
import org.junit.Test
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
import org.leafygreens.kompendium.auth.util.TestData
@ -19,28 +29,17 @@ import org.leafygreens.kompendium.auth.util.TestParams
import org.leafygreens.kompendium.auth.util.TestResponse
import org.leafygreens.kompendium.models.meta.MethodInfo
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.routes.openApi
import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import kotlin.test.AfterTest
import kotlin.test.assertEquals
import org.leafygreens.kompendium.Notarized.notarizedGet
internal class KompendiumAuthTest {
@AfterTest
fun `reset kompendium`() {
Kompendium.openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
Kompendium.cache = emptyMap()
Kompendium.resetSchema()
}
@Test
fun `Notarized Get with basic authentication records all expected information`() {
withTestApplication({
@ -172,7 +171,7 @@ internal class KompendiumAuthTest {
routing {
authenticate(*authenticationConfigName) {
route(TestData.getRoutePath) {
notarizedGet<TestParams, TestResponse>(testGetInfo(*authenticationConfigName)) {
notarizedGet(testGetInfo(*authenticationConfigName)) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
@ -190,8 +189,13 @@ internal class KompendiumAuthTest {
}
private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testGetResponse = ResponseInfo<TestResponse>(KompendiumHttpCodes.OK, "A Successful Endeavor")
fun testGetInfo(vararg security: String) =
MethodInfo("Another get test", "testing more", testGetResponse, securitySchemes = security.toSet())
MethodInfo.GetInfo<TestParams, TestResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse,
securitySchemes = security.toSet()
)
}
}

View File

@ -4,11 +4,11 @@ import java.io.File
object TestData {
object AuthConfigName {
val Basic = "basic"
val JWT = "jwt"
const val Basic = "basic"
const val JWT = "jwt"
}
val getRoutePath = "/test"
const val getRoutePath = "/test"
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"

View File

@ -16,6 +16,7 @@ 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.meta.SchemaMap
import org.leafygreens.kompendium.models.oas.ExampleWrapper
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
@ -45,33 +46,37 @@ object Kompendium {
var pathCalculator: PathCalculator = CorePathCalculator()
// TODO here down is a mess, needs refactor once core functionality is in place
fun MethodInfo.parseMethodInfo(
method: HttpMethod,
fun parseMethodInfo(
info: MethodInfo<*, *>,
paramType: KType,
requestType: KType,
responseType: KType
) = OpenApiSpecPathItemOperation(
summary = this.summary,
description = this.description,
tags = this.tags,
deprecated = this.deprecated,
summary = info.summary,
description = info.description,
tags = info.tags,
deprecated = info.deprecated,
parameters = paramType.toParameterSpec(),
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) }.let {
responses = responseType.toResponseSpec(info.responseInfo)?.let { mapOf(it) }.let {
when (it) {
null -> {
val throwables = parseThrowables(canThrow)
val throwables = parseThrowables(info.canThrow)
when (throwables.isEmpty()) {
true -> null
false -> throwables
}
}
else -> it.plus(parseThrowables(canThrow))
else -> it.plus(parseThrowables(info.canThrow))
}
},
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null,
security = if (this.securitySchemes.isNotEmpty()) listOf(
requestBody = when (info) {
is MethodInfo.PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
is MethodInfo.PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
else -> null
},
security = if (info.securitySchemes.isNotEmpty()) listOf(
// TODO support scopes
this.securitySchemes.associateWith { listOf() }
info.securitySchemes.associateWith { listOf() }
) else null
)
@ -79,7 +84,7 @@ object Kompendium {
errorMap[it.createType()]
}.toMap()
fun ResponseInfo.parseErrorInfo(
fun <TResp> ResponseInfo<TResp>.parseErrorInfo(
errorType: KType,
responseType: KType
) {
@ -87,37 +92,44 @@ object Kompendium {
}
// TODO These two lookin' real similar 👀 Combine?
private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (requestInfo) {
null -> null
else -> {
OpenApiSpecRequest(
description = requestInfo.description,
content = resolveContent(requestInfo.mediaTypes) ?: mapOf()
)
private fun <TReq> KType.toRequestSpec(requestInfo: RequestInfo<TReq>?): OpenApiSpecRequest<TReq>? =
when (requestInfo) {
null -> null
else -> {
OpenApiSpecRequest(
description = requestInfo.description,
content = resolveContent(requestInfo.mediaTypes, requestInfo.examples) ?: mapOf()
)
}
}
}
private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (responseInfo) {
null -> null // TODO again probably revisit this
else -> {
val specResponse = OpenApiSpecResponse(
description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes)
)
Pair(responseInfo.status, specResponse)
private fun <TResp> KType.toResponseSpec(responseInfo: ResponseInfo<TResp>?): Pair<Int, OpenApiSpecResponse<TResp>>? =
when (responseInfo) {
null -> null // TODO again probably revisit this
else -> {
val specResponse = OpenApiSpecResponse(
description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes, responseInfo.examples)
)
Pair(responseInfo.status, specResponse)
}
}
}
private fun KType.resolveContent(mediaTypes: List<String>): Map<String, OpenApiSpecMediaType>? {
private fun <F> KType.resolveContent(
mediaTypes: List<String>,
examples: Map<String, F>
): Map<String, OpenApiSpecMediaType<F>>? {
return if (this != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith {
val ref = getReferenceSlug()
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
OpenApiSpecMediaType(
schema = OpenApiSpecReferenceObject(ref),
examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null }
)
}
} else null
}
// TODO God these annotations make this hideous... any way to improve?
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
val clazz = classifier as KClass<*>
@ -151,7 +163,7 @@ object Kompendium {
}
}
internal fun resetSchema() {
fun resetSchema() {
openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),

View File

@ -15,10 +15,10 @@ import org.leafygreens.kompendium.models.oas.FormatSchema
import org.leafygreens.kompendium.models.oas.ObjectSchema
import org.leafygreens.kompendium.models.oas.ReferencedSchema
import org.leafygreens.kompendium.models.oas.SimpleSchema
import org.leafygreens.kompendium.util.Helpers
import org.leafygreens.kompendium.util.Helpers.COMPONENT_SLUG
import org.leafygreens.kompendium.util.Helpers.genericNameAdapter
import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
import org.leafygreens.kompendium.util.Helpers.logged
import org.slf4j.LoggerFactory
object Kontent {
@ -45,7 +45,7 @@ object Kontent {
fun generateKTypeKontent(
type: KType,
cache: SchemaMap = emptyMap()
): SchemaMap = Helpers.logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
logger.debug("Parsing Kontent of $type")
when (val clazz = type.classifier as KClass<*>) {
Unit::class -> cache

View File

@ -10,7 +10,10 @@ import io.ktor.util.pipeline.PipelineInterceptor
import org.leafygreens.kompendium.Kompendium.parseErrorInfo
import org.leafygreens.kompendium.Kompendium.parseMethodInfo
import org.leafygreens.kompendium.KompendiumPreFlight.errorNotarizationPreFlight
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
@ -18,55 +21,52 @@ object Notarized {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: MethodInfo,
info: GetInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.get =
info.parseMethodInfo(HttpMethod.Get, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Get) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: MethodInfo,
info: PostInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.post =
info.parseMethodInfo(HttpMethod.Post, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Post) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: MethodInfo,
info: PutInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.put =
info.parseMethodInfo(HttpMethod.Put, paramType, requestType, responseType)
parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Put) { handle(body) }
}
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: MethodInfo,
info: DeleteInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.delete =
info.parseMethodInfo(HttpMethod.Delete, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Delete) { handle(body) }
}
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
info: ResponseInfo,
info: ResponseInfo<TResp>,
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
) = errorNotarizationPreFlight<TErr, TResp>() { errorType, responseType ->
info.parseErrorInfo(errorType, responseType)

View File

@ -3,4 +3,4 @@ package org.leafygreens.kompendium.models.meta
import kotlin.reflect.KType
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse>?>
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse<*>>?>

View File

@ -2,14 +2,94 @@ package org.leafygreens.kompendium.models.meta
import kotlin.reflect.KClass
// 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,
val securitySchemes: Set<String> = emptySet(),
val canThrow: Set<KClass<*>> = emptySet()
)
sealed class MethodInfo<TParam, TResp>(
open val summary: String,
open val description: String? = null,
open val tags: Set<String> = emptySet(),
open val deprecated: Boolean = false,
open val securitySchemes: Set<String> = emptySet(),
open val canThrow: Set<KClass<*>> = emptySet(),
open val responseInfo: ResponseInfo<TResp>? = null,
open val parameterExamples: Map<String, TParam> = emptyMap(),
) {
data class GetInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples
)
data class PostInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples
)
data class PutInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples
)
data class DeleteInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples
)
}

View File

@ -1,7 +1,8 @@
package org.leafygreens.kompendium.models.meta
data class RequestInfo(
data class RequestInfo<TReq>(
val description: String,
val required: Boolean = true,
val mediaTypes: List<String> = listOf("application/json")
val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TReq> = emptyMap()
)

View File

@ -1,7 +1,8 @@
package org.leafygreens.kompendium.models.meta
data class ResponseInfo(
data class ResponseInfo<TResp>(
val status: Int,
val description: String,
val mediaTypes: List<String> = listOf("application/json")
val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TResp> = emptyMap()
)

View File

@ -1,16 +1,8 @@
package org.leafygreens.kompendium.models.oas
// TODO Oof -> https://swagger.io/specification/#media-type-object
sealed class OpenApiSpecMediaType {
data class Explicit(
val schema: OpenApiSpecSchema, // TODO sheesh -> https://swagger.io/specification/#schema-object
val example: String? = null, // TODO Enforce type? then serialize?
val examples: Map<String, String>? = null, // needs to be mutually exclusive with example
val encoding: Map<String, String>? = null // todo encoding object -> https://swagger.io/specification/#encoding-object
) : OpenApiSpecMediaType()
data class Referenced(
val schema: OpenApiSpecReferenceObject
) : OpenApiSpecMediaType()
}
data class OpenApiSpecMediaType<T>(
val schema: OpenApiSpecReferenceObject,
val examples: Map<String, ExampleWrapper<T>>? = null
)
data class ExampleWrapper<T>(val value: T)

View File

@ -4,23 +4,13 @@ sealed class OpenApiSpecReferencable
class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable()
data class OpenApiSpecCallback(
val todo: String // todo fuck me -> https://swagger.io/specification/#callback-object
) : OpenApiSpecReferencable()
data class OpenApiSpecResponse(
data class OpenApiSpecResponse<T>(
val description: String? = null,
val headers: Map<String, OpenApiSpecReferencable>? = null,
val content: Map<String, OpenApiSpecMediaType>? = null,
val content: Map<String, OpenApiSpecMediaType<T>>? = null,
val links: Map<String, OpenApiSpecReferencable>? = null
) : OpenApiSpecReferencable()
data class OpenApiSpecHeader(
val name: String,
val description: String?,
val externalDocs: OpenApiSpecExternalDocumentation?
) : OpenApiSpecReferencable()
data class OpenApiSpecParameter(
val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
@ -33,8 +23,8 @@ data class OpenApiSpecParameter(
val explode: Boolean? = null
) : OpenApiSpecReferencable()
data class OpenApiSpecRequest(
data class OpenApiSpecRequest<T>(
val description: String?,
val content: Map<String, OpenApiSpecMediaType>,
val content: Map<String, OpenApiSpecMediaType<T>>,
val required: Boolean = false
) : OpenApiSpecReferencable()

View File

@ -26,7 +26,10 @@ import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.Notarized.notarizedPost
import org.leafygreens.kompendium.Notarized.notarizedPut
import org.leafygreens.kompendium.annotations.QueryParam
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
@ -39,7 +42,8 @@ import org.leafygreens.kompendium.util.ComplexRequest
import org.leafygreens.kompendium.util.ExceptionResponse
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import org.leafygreens.kompendium.util.TestCreatedResponse
import org.leafygreens.kompendium.util.TestData
import org.leafygreens.kompendium.util.TestHelpers.getFileSnapshot
import org.leafygreens.kompendium.util.TestNested
import org.leafygreens.kompendium.util.TestParams
import org.leafygreens.kompendium.util.TestRequest
import org.leafygreens.kompendium.util.TestResponse
@ -67,7 +71,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get.json").trim()
val expected = getFileSnapshot("notarized_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -99,7 +103,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_post.json").trim()
val expected = getFileSnapshot("notarized_post.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -131,7 +135,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_put.json").trim()
val expected = getFileSnapshot("notarized_put.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -164,7 +168,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_delete.json").trim()
val expected = getFileSnapshot("notarized_delete.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -195,7 +199,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("path_parser.json").trim()
val expected = getFileSnapshot("path_parser.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -211,7 +215,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("root_route.json").trim()
val expected = getFileSnapshot("root_route.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -243,7 +247,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("nested_under_root.json").trim()
val expected = getFileSnapshot("nested_under_root.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -259,7 +263,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("trailing_slash.json").trim()
val expected = getFileSnapshot("trailing_slash.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -291,7 +295,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("complex_type.json").trim()
val expected = getFileSnapshot("complex_type.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -307,7 +311,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_primitives.json").trim()
val expected = getFileSnapshot("notarized_primitives.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -323,7 +327,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("response_list.json").trim()
val expected = getFileSnapshot("response_list.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -339,7 +343,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("no_request_params_and_no_response_body.json").trim()
val expected = getFileSnapshot("no_request_params_and_no_response_body.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -355,7 +359,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("non_required_params.json").trim()
val expected = getFileSnapshot("non_required_params.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -367,12 +371,11 @@ internal class KompendiumTest {
docs()
returnsList()
}) {
// do
val html = handleRequest(HttpMethod.Get, "/docs").response.content
// expected
val expected = TestData.getFileSnapshot("redoc.html")
val expected = getFileSnapshot("redoc.html")
assertEquals(expected, html)
}
}
@ -389,13 +392,11 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_with_exception_response.json").trim()
val expected = getFileSnapshot("notarized_get_with_exception_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Generates additional responses when passed multiple throwables`() {
withTestApplication({
@ -408,28 +409,52 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_with_multiple_exception_responses.json").trim()
val expected = getFileSnapshot("notarized_get_with_multiple_exception_responses.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate example response and request bodies`() {
withTestApplication({
configModule()
docs()
withExamples()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("example_req_and_resp.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testGetResponse = ResponseInfo<TestResponse>(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testGetListResponse = ResponseInfo<List<TestResponse>>(KompendiumHttpCodes.OK, "A Successful List-y Endeavor")
val testPostResponse = ResponseInfo<TestCreatedResponse>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testPostResponseAgain = ResponseInfo<Boolean>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testDeleteResponse =
ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo("A Test request")
val testGetInfo = MethodInfo("Another get test", "testing more", testGetResponse)
ResponseInfo<Unit>(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo<TestRequest>("A Test request")
val testRequestAgain = RequestInfo<Int>("A Test request")
val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
val testGetInfo = GetInfo<TestParams, TestResponse>(summary = "Another get test", description = "testing more", responseInfo = testGetResponse)
val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(summary = "Another get test", description = "testing more", responseInfo = testGetListResponse)
val testGetWithException = testGetInfo.copy(
canThrow = setOf(Exception::class)
)
val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class)
)
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)
val emptyTestGetInfo = MethodInfo("No request params and response body", "testing more")
val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(summary = "Test post endpoint", description = "Post your tests here!", responseInfo = testPostResponse, requestInfo = testRequest)
val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = complexRequest)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = testRequest)
val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponseAgain, requestInfo = testRequestAgain)
val testDeleteInfo = DeleteInfo<TestParams, Unit>(summary = "Test delete endpoint", description = "testing my deletes", responseInfo = testDeleteResponse)
val emptyTestGetInfo = GetInfo<OptionalParams, Unit>(summary = "No request params and response body", description = "testing more")
val trulyEmptyTestGetInfo = GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more")
}
private fun Application.configModule() {
@ -463,7 +488,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetWithNotarizedException() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithException) {
notarizedGet(testGetWithException) {
error("something terrible has happened!")
}
}
@ -473,7 +498,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetWithMultipleThrowables() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithMultipleExceptions) {
notarizedGet(testGetWithMultipleExceptions) {
error("something terrible has happened!")
}
}
@ -483,7 +508,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetModule() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
@ -493,7 +518,7 @@ internal class KompendiumTest {
private fun Application.notarizedPostModule() {
routing {
route("/test") {
notarizedPost<TestParams, TestRequest, TestCreatedResponse>(testPostInfo) {
notarizedPost(testPostInfo) {
call.respondText { "hey dude ✌️ congratz on the post request" }
}
}
@ -503,7 +528,7 @@ internal class KompendiumTest {
private fun Application.notarizedDeleteModule() {
routing {
route("/test") {
notarizedDelete<TestParams, Unit>(testDeleteInfo) {
notarizedDelete(testDeleteInfo) {
call.respond(HttpStatusCode.NoContent)
}
}
@ -513,7 +538,7 @@ internal class KompendiumTest {
private fun Application.notarizedPutModule() {
routing {
route("/test") {
notarizedPut<TestParams, TestRequest, TestCreatedResponse>(testPutInfo) {
notarizedPut(testPutInfoAlso) {
call.respondText { "hey pal 🌝 whatcha doin' here?" }
}
}
@ -528,7 +553,7 @@ internal class KompendiumTest {
route("/complex") {
route("path") {
route("with/an/{id}") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "Aww you followed this whole route 🥺" }
}
}
@ -543,7 +568,7 @@ internal class KompendiumTest {
private fun Application.rootModule() {
routing {
route("/") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "☎️🏠🌲" }
}
}
@ -554,7 +579,7 @@ internal class KompendiumTest {
routing {
route("/") {
route("/testerino") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "🤔🔥" }
}
}
@ -566,7 +591,7 @@ internal class KompendiumTest {
routing {
route("/test") {
route("/") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "🙀👾" }
}
}
@ -577,7 +602,7 @@ internal class KompendiumTest {
private fun Application.returnsList() {
routing {
route("/test") {
notarizedGet<TestParams, List<TestResponse>>(testGetInfo) {
notarizedGet(testGetInfoAgain) {
call.respondText { "hey dude ur doing amazing work!" }
}
}
@ -587,7 +612,7 @@ internal class KompendiumTest {
private fun Application.complexType() {
routing {
route("/test") {
notarizedPut<Unit, ComplexRequest, TestResponse>(testPutInfo) {
notarizedPut(testPutInfo) {
call.respondText { "heya" }
}
}
@ -597,7 +622,7 @@ internal class KompendiumTest {
private fun Application.primitives() {
routing {
route("/test") {
notarizedPut<Unit, Int, Boolean>(testPutInfo) {
notarizedPut(testPutInfoAgain) {
call.respondText { "heya" }
}
}
@ -607,7 +632,32 @@ internal class KompendiumTest {
private fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet<Unit, Unit>(emptyTestGetInfo) {
notarizedGet(trulyEmptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private fun Application.withExamples() {
routing {
route("/test/examples") {
notarizedPost(info = PostInfo<Unit, TestRequest, TestResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
requestInfo = RequestInfo(
description = "Test",
examples = mapOf(
"one" to TestRequest(fieldName = TestNested(nesty = "hey"), b = 4.0, aaa = emptyList()),
"two" to TestRequest(fieldName = TestNested(nesty = "hello"), b = 3.8, aaa = listOf(31324234))
)
),
responseInfo = ResponseInfo(
status = 201,
description = "nice",
examples = mapOf("test" to TestResponse(c = "spud"))
),
)) {
call.respond(HttpStatusCode.OK)
}
}
@ -618,7 +668,7 @@ internal class KompendiumTest {
private fun Application.nonRequiredParamsGet() {
routing {
route("/test/optional") {
notarizedGet<OptionalParams, Unit>(emptyTestGetInfo) {
notarizedGet(emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}

View File

@ -1,27 +0,0 @@
package org.leafygreens.kompendium.models
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlin.test.Test
import kotlin.test.assertEquals
import org.leafygreens.kompendium.util.TestData
internal class OpenApiSpecTest {
private val mapper = jacksonObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.writerWithDefaultPrettyPrinter()
@Test
fun `OpenApiSpec can be serialized into a valid Open API Spec`() {
// when
val spec = TestData.testSpec
// do
val json = mapper.writeValueAsString(spec)
// expect
val expected = TestData.getFileSnapshot("petstore.json").trim()
assertEquals(expected, json, "Should serialize an empty spec")
}
}

View File

@ -1,227 +0,0 @@
package org.leafygreens.kompendium.util
import java.io.File
import java.net.URI
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponents
import org.leafygreens.kompendium.models.oas.OpenApiSpecExternalDocumentation
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.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
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.models.oas.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.models.oas.OpenApiSpecTag
object TestData {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
val testSpec = OpenApiSpec(
info = OpenApiSpecInfo(
title = "Swagger Petstore",
description = """
This is a sample server Petstore server. You can find out more about Swagger at
[http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).
For this sample, you can use the api key `special-key` to test the authorization filters.
""".trimIndent(),
termsOfService = URI("http://swagger.io/terms/"),
contact = OpenApiSpecInfoContact(
name = "Team Swag",
email = "apiteam@swagger.io"
),
license = OpenApiSpecInfoLicense(
name = "Apache 2.0",
url = URI("http://www.apache.org/licenses/LICENSE-2.0.html")
),
version = "1.0.0"
),
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more about Swagger",
url = URI("http://swagger.io")
),
servers = mutableListOf(
OpenApiSpecServer(
url = URI("https://petstore.swagger.io/v2")
),
OpenApiSpecServer(
url = URI("http://petstore.swagger.io/v2")
)
),
tags = mutableListOf(
OpenApiSpecTag(
name = "pet",
description = "Everything about your Pets",
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more",
url = URI("http://swagger.io")
)
),
OpenApiSpecTag(
name = "store",
description = "Access to Petstore orders"
),
OpenApiSpecTag(
name = "user",
description = "Operations about user",
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more about our store",
url = URI("http://swagger.io")
)
)
),
paths = mutableMapOf(
"/pet" to OpenApiSpecPathItem(
put = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Update an existing pet",
operationId = "updatePet",
requestBody = OpenApiSpecRequest(
description = "Pet object that needs to be added to the store",
content = mapOf(
"application/json" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
),
"application/xml" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
)
),
required = true
),
responses = mapOf(
400 to OpenApiSpecResponse(
description = "Invalid ID supplied",
content = emptyMap()
),
404 to OpenApiSpecResponse(
description = "Pet not found",
content = emptyMap()
),
405 to OpenApiSpecResponse(
description = "Validation exception",
content = emptyMap()
)
),
security = listOf(
mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
)
),
`x-codegen-request-body-name` = "body"
),
post = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Add a new pet to the store",
operationId = "addPet",
requestBody = OpenApiSpecRequest(
description = "Pet object that needs to be added to the store",
content = mapOf(
"application/json" to OpenApiSpecMediaType.Referenced(
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
),
"application/xml" to OpenApiSpecMediaType.Referenced(
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
)
)
),
responses = mapOf(
405 to OpenApiSpecResponse(
description = "Invalid Input",
content = emptyMap()
)
),
security = listOf(
mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
)
),
`x-codegen-request-body-name` = "body"
)
),
"/pet/findByStatus" to OpenApiSpecPathItem(
get = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Find Pets by status",
description = "Multiple status values can be provided with comma separated strings",
operationId = "findPetsByStatus",
parameters = listOf(
OpenApiSpecParameter(
name = "status",
`in` = "query",
description = "Status values that need to be considered for filter",
required = true,
style = "form",
explode = true,
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaString(
default = "available",
`enum` = setOf("available", "pending", "sold")
)
)
)
),
responses = mapOf(
200 to OpenApiSpecResponse(
description = "successful operation",
content = mapOf(
"application/xml" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
)
),
"application/json" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
)
)
)
),
400 to OpenApiSpecResponse(
description = "Invalid status value",
content = mapOf()
)
),
security = listOf(mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
))
)
)
),
components = OpenApiSpecComponents(
securitySchemes = mutableMapOf(
"petstore_auth" to OpenApiSpecSchemaSecurity(
type = "oauth2",
flows = OpenApiSpecOAuthFlows(
implicit = OpenApiSpecOAuthFlow(
authorizationUrl = URI("http://petstore.swagger.io/oauth/dialog"),
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
),
"api_key" to OpenApiSpecSchemaSecurity(
type = "apiKey",
name = "api_key",
`in` = "header"
)
),
schemas = mutableMapOf()
)
)
}

View File

@ -0,0 +1,33 @@
package org.leafygreens.kompendium.util
import java.io.File
import java.net.URI
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponents
import org.leafygreens.kompendium.models.oas.OpenApiSpecExternalDocumentation
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.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
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.models.oas.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.models.oas.OpenApiSpecTag
object TestHelpers {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
}

View File

@ -30,7 +30,7 @@
"description" : "Put your tests here!",
"parameters" : [ ],
"requestBody" : {
"description" : "A Test request",
"description" : "A Complex request",
"content" : {
"application/json" : {
"schema" : {
@ -46,7 +46,7 @@
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
"$ref" : "#/components/schemas/TestCreatedResponse"
}
}
}
@ -61,10 +61,17 @@
"String" : {
"type" : "string"
},
"TestResponse" : {
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"

View File

@ -0,0 +1,140 @@
{
"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/examples" : {
"post" : {
"tags" : [ ],
"summary" : "Example Parameters",
"description" : "A test for setting parameter examples",
"parameters" : [ ],
"requestBody" : {
"description" : "Test",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestRequest"
},
"examples" : {
"one" : {
"value" : {
"fieldName" : {
"nesty" : "hey"
},
"b" : 4.0,
"aaa" : [ ]
}
},
"two" : {
"value" : {
"fieldName" : {
"nesty" : "hello"
},
"b" : 3.8,
"aaa" : [ 31324234 ]
}
}
}
}
},
"required" : false
},
"responses" : {
"201" : {
"description" : "nice",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
},
"examples" : {
"test" : {
"value" : {
"c" : "spud"
}
}
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Long" : {
"format" : "int64",
"type" : "integer"
},
"List-Long" : {
"items" : {
"$ref" : "#/components/schemas/Long"
},
"type" : "array"
},
"Double" : {
"format" : "double",
"type" : "number"
},
"TestNested" : {
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"TestRequest" : {
"properties" : {
"aaa" : {
"$ref" : "#/components/schemas/List-Long"
},
"b" : {
"$ref" : "#/components/schemas/Double"
},
"fieldName" : {
"$ref" : "#/components/schemas/TestNested"
}
},
"type" : "object"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -47,7 +47,7 @@
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"description" : "A Successful List-y Endeavor",
"content" : {
"application/json" : {
"schema" : {

View File

@ -30,7 +30,10 @@ import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
@ -38,6 +41,7 @@ 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.playground.KompendiumTOC.testAuthenticatedSingleGetInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testGetWithExamples
import org.leafygreens.kompendium.playground.KompendiumTOC.testIdGetInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
@ -86,8 +90,8 @@ fun main() {
}
var featuresInstalled = false
fun Application.mainModule() {
// only install once in case of auto reload
fun Application.configModule() {
if (!featuresInstalled) {
install(ContentNegotiation) {
jackson {
@ -120,33 +124,42 @@ fun Application.mainModule() {
}
featuresInstalled = true
}
}
fun Application.mainModule() {
configModule()
routing {
openApi(oas)
redoc(oas)
swaggerUI()
route("/potato/spud") {
notarizedGet(testGetWithExamples) {
call.respond(HttpStatusCode.OK)
}
}
route("/test") {
route("/{id}") {
notarizedGet<ExampleParams, ExampleResponse>(testIdGetInfo) {
notarizedGet(testIdGetInfo) {
call.respondText("get by id")
}
}
route("/single") {
notarizedGet<Unit, ExampleResponse>(testSingleGetInfo) {
notarizedGet(testSingleGetInfo) {
call.respondText("get single")
}
notarizedPost<Unit, ExampleRequest, ExampleCreatedResponse>(testSinglePostInfo) {
notarizedPost(testSinglePostInfo) {
call.respondText("test post")
}
notarizedPut<JustQuery, ExampleRequest, ExampleCreatedResponse>(testSinglePutInfo) {
notarizedPut(testSinglePutInfo) {
call.respondText { "hey" }
}
notarizedDelete<Unit, Unit>(testSingleDeleteInfo) {
notarizedDelete(testSingleDeleteInfo) {
call.respondText { "heya" }
}
}
authenticate("basic") {
route("/authenticated/single") {
notarizedGet<Unit, Unit>(testAuthenticatedSingleGetInfo) {
notarizedGet(testAuthenticatedSingleGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
@ -186,7 +199,16 @@ data class ExceptionResponse(val message: String)
data class ExampleCreatedResponse(val id: Int, val c: String)
object KompendiumTOC {
val testIdGetInfo = MethodInfo(
val testGetWithExamples = GetInfo<Unit, ExampleResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
responseInfo = ResponseInfo(
status = 200,
description = "nice",
examples = mapOf("test" to ExampleResponse(c = "spud"))
),
)
val testIdGetInfo = GetInfo<ExampleParams, ExampleResponse>(
summary = "Get Test",
description = "Test for the getting",
tags = setOf("test", "sample", "get"),
@ -195,7 +217,7 @@ object KompendiumTOC {
description = "Returns sample info"
)
)
val testSingleGetInfo = MethodInfo(
val testSingleGetInfo = GetInfo<Unit, ExampleResponse>(
summary = "Another get test",
description = "testing more",
tags = setOf("anotherTest", "sample"),
@ -208,7 +230,7 @@ object KompendiumTOC {
summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class)
)
val testSinglePostInfo = MethodInfo(
val testSinglePostInfo = PostInfo<Unit, ExampleRequest, ExampleCreatedResponse>(
summary = "Test post endpoint",
description = "Post your tests here!",
requestInfo = RequestInfo(
@ -219,7 +241,7 @@ object KompendiumTOC {
description = "Worlds most complex response"
)
)
val testSinglePutInfo = MethodInfo(
val testSinglePutInfo = PutInfo<JustQuery, ExampleRequest, ExampleCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
requestInfo = RequestInfo(
@ -230,7 +252,7 @@ object KompendiumTOC {
description = "What we give you when u do the puts"
)
)
val testSingleDeleteInfo = MethodInfo(
val testSingleDeleteInfo = DeleteInfo<Unit, Unit>(
summary = "Test delete endpoint",
description = "testing my deletes",
responseInfo = ResponseInfo(
@ -239,7 +261,7 @@ object KompendiumTOC {
mediaTypes = emptyList()
)
)
val testAuthenticatedSingleGetInfo = MethodInfo(
val testAuthenticatedSingleGetInfo = GetInfo<Unit, Unit>(
summary = "Another get test",
description = "testing more",
tags = setOf("anotherTest", "sample"),