More of the core functionality (#6)
This commit is contained in:
@ -10,6 +10,7 @@ dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
||||
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
@ -3,7 +3,6 @@ package org.leafygreens.kompendium
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.createRouteFromPath
|
||||
import io.ktor.routing.method
|
||||
import io.ktor.util.pipeline.PipelineInterceptor
|
||||
import java.lang.reflect.ParameterizedType
|
||||
@ -13,83 +12,128 @@ import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||
import org.leafygreens.kompendium.annotations.KompendiumInternal
|
||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.models.oas.ArraySchema
|
||||
import org.leafygreens.kompendium.models.oas.FormatSchema
|
||||
import org.leafygreens.kompendium.models.oas.ObjectSchema
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpec
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponentSchema
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
|
||||
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.SimpleSchema
|
||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.util.Helpers.calculatePath
|
||||
import org.leafygreens.kompendium.util.Helpers.putPairIfAbsent
|
||||
|
||||
object Kompendium {
|
||||
val openApiSpec = OpenApiSpec(
|
||||
|
||||
const val COMPONENT_SLUG = "#/components/schemas"
|
||||
|
||||
var openApiSpec = OpenApiSpec(
|
||||
info = OpenApiSpecInfo(),
|
||||
servers = mutableListOf(),
|
||||
paths = mutableMapOf()
|
||||
)
|
||||
|
||||
fun Route.notarizedGet(info: MethodInfo, body: PipelineInterceptor<Unit, ApplicationCall>): Route {
|
||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<TParam, Unit, TResp>() {
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.get = OpenApiSpecPathItemOperation(
|
||||
summary = info.summary,
|
||||
description = info.description,
|
||||
tags = info.tags
|
||||
)
|
||||
openApiSpec.paths[path]?.get = info.parseMethodInfo<Unit, TResp>()
|
||||
return method(HttpMethod.Get) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> Route.notarizedPost(
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<TQ, TP, TR>(info, body) { i, b ->
|
||||
): Route = generateComponentSchemas<TParam, TReq, TResp>() {
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.post = OpenApiSpecPathItemOperation(
|
||||
summary = i.summary,
|
||||
description = i.description,
|
||||
tags = i.tags
|
||||
)
|
||||
return method(HttpMethod.Post) { handle(b) }
|
||||
openApiSpec.paths[path]?.post = info.parseMethodInfo<TReq, TResp>()
|
||||
return method(HttpMethod.Post) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> Route.notarizedPut(
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
): Route = generateComponentSchemas<TQ, TP, TR>(info, body) { i, b ->
|
||||
): Route = generateComponentSchemas<TParam, TReq, TResp>() {
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.put = OpenApiSpecPathItemOperation(
|
||||
summary = i.summary,
|
||||
description = i.description,
|
||||
tags = i.tags
|
||||
)
|
||||
return method(HttpMethod.Put) { handle(b) }
|
||||
openApiSpec.paths[path]?.put = info.parseMethodInfo<TReq, TResp>()
|
||||
return method(HttpMethod.Put) { handle(body) }
|
||||
}
|
||||
|
||||
@OptIn(KompendiumInternal::class)
|
||||
inline fun <reified TQ : Any, reified TP : Any, reified TR : Any> generateComponentSchemas(
|
||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
||||
info: MethodInfo,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
block: (MethodInfo, PipelineInterceptor<Unit, ApplicationCall>) -> Route
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route = generateComponentSchemas<TParam, Unit, TResp> {
|
||||
val path = calculatePath()
|
||||
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
openApiSpec.paths[path]?.delete = info.parseMethodInfo<Unit, TResp>()
|
||||
return method(HttpMethod.Delete) { handle(body) }
|
||||
}
|
||||
|
||||
inline fun <reified TReq, reified TResp> MethodInfo.parseMethodInfo() = OpenApiSpecPathItemOperation(
|
||||
summary = this.summary,
|
||||
description = this.description,
|
||||
tags = this.tags,
|
||||
deprecated = this.deprecated,
|
||||
responses = parseResponseAnnotation<TResp>()?.let { mapOf(it) },
|
||||
requestBody = parseRequestAnnotation<TReq>()
|
||||
)
|
||||
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> generateComponentSchemas(
|
||||
block: () -> Route
|
||||
): Route {
|
||||
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TQ::class))
|
||||
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TR::class))
|
||||
openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TP::class))
|
||||
return block.invoke(info, body)
|
||||
if (TResp::class != Unit::class) openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TResp::class))
|
||||
if (TReq::class != Unit::class) openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TReq::class))
|
||||
// openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TParam::class))
|
||||
return block.invoke()
|
||||
}
|
||||
|
||||
inline fun <reified TReq> parseRequestAnnotation(): OpenApiSpecRequest? = when (TReq::class) {
|
||||
Unit::class -> null
|
||||
else -> {
|
||||
val anny = TReq::class.findAnnotation<KompendiumRequest>() ?: error("My way or the highway bub")
|
||||
OpenApiSpecRequest(
|
||||
description = anny.description,
|
||||
content = anny.mediaTypes.associate {
|
||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TReq::class.simpleName}")
|
||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
||||
Pair(it, mediaType)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified TResp> parseResponseAnnotation(): Pair<Int, OpenApiSpecResponse>? = when (TResp::class) {
|
||||
Unit::class -> null
|
||||
else -> {
|
||||
val anny = TResp::class.findAnnotation<KompendiumResponse>() ?: error("My way or the highway bub")
|
||||
val specResponse = OpenApiSpecResponse(
|
||||
description = anny.description,
|
||||
content = anny.mediaTypes.associate {
|
||||
val ref = OpenApiSpecReferenceObject("$COMPONENT_SLUG/${TResp::class.simpleName}")
|
||||
val mediaType = OpenApiSpecMediaType.Referenced(ref)
|
||||
Pair(it, mediaType)
|
||||
}
|
||||
)
|
||||
Pair(anny.status, specResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@KompendiumInternal
|
||||
// TODO Investigate a caching mechanism to reduce overhead... then just reference once created
|
||||
fun objectSchemaPair(clazz: KClass<*>): Pair<String, ObjectSchema> {
|
||||
val o = objectSchema(clazz)
|
||||
return Pair(clazz.qualifiedName!!, o)
|
||||
return Pair(clazz.simpleName!!, o)
|
||||
}
|
||||
|
||||
private fun objectSchema(clazz: KClass<*>): ObjectSchema =
|
||||
@ -115,7 +159,6 @@ object Kompendium {
|
||||
return ArraySchema(fieldToSchema(listType))
|
||||
}
|
||||
|
||||
@OptIn(KompendiumInternal::class)
|
||||
private fun fieldToSchema(field: KClass<*>): OpenApiSpecComponentSchema = when (field) {
|
||||
Int::class -> FormatSchema("int32", "integer")
|
||||
Long::class -> FormatSchema("int64", "integer")
|
||||
@ -125,4 +168,12 @@ object Kompendium {
|
||||
Boolean::class -> SimpleSchema("boolean")
|
||||
else -> objectSchema(field)
|
||||
}
|
||||
|
||||
internal fun resetSchema() {
|
||||
openApiSpec = OpenApiSpec(
|
||||
info = OpenApiSpecInfo(),
|
||||
servers = mutableListOf(),
|
||||
paths = mutableMapOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
annotation class KompendiumContact(
|
||||
val name: String,
|
||||
val url: String = "",
|
||||
val email: String = ""
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
annotation class KompendiumInfo(
|
||||
val title: String,
|
||||
val version: String,
|
||||
val description: String = "",
|
||||
val termsOfService: String = ""
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.WARNING,
|
||||
message = "This API internal to Kompendium and should not be used. It could be removed or changed without notice."
|
||||
)
|
||||
@Experimental(level = Experimental.Level.WARNING)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.PROPERTY_SETTER
|
||||
)
|
||||
annotation class KompendiumInternal
|
@ -1,5 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class KompendiumModule
|
@ -0,0 +1,7 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
annotation class KompendiumRequest(
|
||||
val description: String,
|
||||
val required: Boolean = true,
|
||||
val mediaTypes: Array<String> = ["application/json"]
|
||||
)
|
@ -0,0 +1,9 @@
|
||||
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,7 +0,0 @@
|
||||
package org.leafygreens.kompendium.annotations
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
annotation class KompendiumServers(
|
||||
val urls: Array<String>
|
||||
)
|
@ -1,3 +1,8 @@
|
||||
package org.leafygreens.kompendium.models.meta
|
||||
|
||||
data class MethodInfo(val summary: String, val description: String? = null, val tags: Set<String> = emptySet())
|
||||
data class MethodInfo(
|
||||
val summary: String,
|
||||
val description: String? = null,
|
||||
val tags: Set<String> = emptySet(),
|
||||
val deprecated: Boolean = false
|
||||
)
|
||||
|
@ -1,9 +1,16 @@
|
||||
package org.leafygreens.kompendium.models.oas
|
||||
|
||||
// TODO Oof -> https://swagger.io/specification/#media-type-object
|
||||
data class OpenApiSpecMediaType(
|
||||
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
|
||||
)
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ data class OpenApiSpecPathItemOperation(
|
||||
var parameters: List<OpenApiSpecReferencable>? = null,
|
||||
var requestBody: OpenApiSpecReferencable? = null,
|
||||
// TODO How to enforce `default` requirement 🧐
|
||||
var responses: Map<String, OpenApiSpecReferencable>? = null,
|
||||
var responses: Map<Int, OpenApiSpecReferencable>? = null,
|
||||
var callbacks: Map<String, OpenApiSpecReferencable>? = null,
|
||||
var deprecated: Boolean = false,
|
||||
// todo big yikes... also needs to reference objects in the security scheme 🤔
|
||||
|
@ -1,13 +1,311 @@
|
||||
package org.leafygreens.kompendium
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.call
|
||||
import io.ktor.application.install
|
||||
import io.ktor.features.ContentNegotiation
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondText
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.routing.route
|
||||
import io.ktor.routing.routing
|
||||
import io.ktor.server.testing.handleRequest
|
||||
import io.ktor.server.testing.withTestApplication
|
||||
import java.net.URI
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
||||
import org.leafygreens.kompendium.util.TestCreatedResponse
|
||||
import org.leafygreens.kompendium.util.TestData
|
||||
import org.leafygreens.kompendium.util.TestDeleteResponse
|
||||
import org.leafygreens.kompendium.util.TestParams
|
||||
import org.leafygreens.kompendium.util.TestRequest
|
||||
import org.leafygreens.kompendium.util.TestResponse
|
||||
|
||||
internal class KompendiumTest {
|
||||
|
||||
@AfterTest
|
||||
fun `reset kompendium`() {
|
||||
Kompendium.resetSchema()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Kompendium can be instantiated with no details`() {
|
||||
assertEquals(Kompendium.openApiSpec.openapi, "3.0.3", "Kompendium has a default spec version of 3.0.3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Get records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedGetModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("notarized_get.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Get does not interrupt the pipeline`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedGetModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/test").response.content
|
||||
|
||||
// expect
|
||||
val expected = "hey dude ‼️ congratz on the get request"
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Post records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedPostModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("notarized_post.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Notarized post does not interrupt the pipeline`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedPostModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Post, "/test").response.content
|
||||
|
||||
// expect
|
||||
val expected = "hey dude ✌️ congratz on the post request"
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Put records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedPutModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("notarized_put.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Notarized put does not interrupt the pipeline`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedPutModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Put, "/test").response.content
|
||||
|
||||
// expect
|
||||
val expected = "hey pal 🌝 whatcha doin' here?"
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized delete records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedDeleteModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("notarized_delete.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Notarized delete does not interrupt the pipeline`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
notarizedDeleteModule()
|
||||
}) {
|
||||
// do
|
||||
val status = handleRequest(HttpMethod.Delete, "/test").response.status()
|
||||
|
||||
// expect
|
||||
assertEquals(HttpStatusCode.NoContent, status, "No content status should be received")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Path parser stores the expected path`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
openApiModule()
|
||||
pathParsingTestModule()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = TestData.getFileSnapshot("path_parser.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val testGetInfo = MethodInfo("Another get test", "testing more")
|
||||
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!")
|
||||
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!")
|
||||
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes")
|
||||
}
|
||||
|
||||
private fun Application.configModule() {
|
||||
install(ContentNegotiation) {
|
||||
jackson {
|
||||
enable(SerializationFeature.INDENT_OUTPUT)
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.notarizedGetModule() {
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedGet<TestParams, TestResponse>(testGetInfo) {
|
||||
call.respondText { "hey dude ‼️ congratz on the get request" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.notarizedPostModule() {
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedPost<TestParams, TestRequest, TestCreatedResponse>(testPostInfo) {
|
||||
call.respondText { "hey dude ✌️ congratz on the post request" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.notarizedDeleteModule() {
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedDelete<TestParams, TestDeleteResponse>(testDeleteInfo) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.notarizedPutModule() {
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedPut<TestParams, TestRequest, TestCreatedResponse>(testPutInfo) {
|
||||
call.respondText { "hey pal 🌝 whatcha doin' here?" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.pathParsingTestModule() {
|
||||
routing {
|
||||
route("/this") {
|
||||
route("/is") {
|
||||
route("/a") {
|
||||
route("/complex") {
|
||||
route("path") {
|
||||
route("with/an/{id}") {
|
||||
notarizedGet<TestParams, TestResponse>(testGetInfo) {
|
||||
call.respondText { "Aww you followed this whole route 🥺" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.openApiModule() {
|
||||
routing {
|
||||
route("/openapi.json") {
|
||||
get {
|
||||
call.respond(
|
||||
Kompendium.openApiSpec.copy(
|
||||
info = OpenApiSpecInfo(
|
||||
title = "Test API",
|
||||
version = "1.33.7",
|
||||
description = "An amazing, fully-ish 😉 generated API spec",
|
||||
termsOfService = URI("https://example.com"),
|
||||
contact = OpenApiSpecInfoContact(
|
||||
name = "Homer Simpson",
|
||||
email = "chunkylover53@aol.com",
|
||||
url = URI("https://gph.is/1NPUDiM")
|
||||
),
|
||||
license = OpenApiSpecInfoLicense(
|
||||
name = "MIT",
|
||||
url = URI("https://github.com/lg-backbone/kompendium/blob/main/LICENSE")
|
||||
)
|
||||
),
|
||||
servers = mutableListOf(
|
||||
OpenApiSpecServer(
|
||||
url = URI("https://myawesomeapi.com"),
|
||||
description = "Production instance of my API"
|
||||
),
|
||||
OpenApiSpecServer(
|
||||
url = URI("https://staging.myawesomeapi.com"),
|
||||
description = "Where the fun stuff happens"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ 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
|
||||
@ -92,25 +93,25 @@ object TestData {
|
||||
requestBody = OpenApiSpecRequest(
|
||||
description = "Pet object that needs to be added to the store",
|
||||
content = mapOf(
|
||||
"application/json" to OpenApiSpecMediaType(
|
||||
"application/json" to OpenApiSpecMediaType.Explicit(
|
||||
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
|
||||
),
|
||||
"application/xml" to OpenApiSpecMediaType(
|
||||
"application/xml" to OpenApiSpecMediaType.Explicit(
|
||||
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
|
||||
)
|
||||
),
|
||||
required = true
|
||||
),
|
||||
responses = mapOf(
|
||||
"400" to OpenApiSpecResponse(
|
||||
400 to OpenApiSpecResponse(
|
||||
description = "Invalid ID supplied",
|
||||
content = emptyMap()
|
||||
),
|
||||
"404" to OpenApiSpecResponse(
|
||||
404 to OpenApiSpecResponse(
|
||||
description = "Pet not found",
|
||||
content = emptyMap()
|
||||
),
|
||||
"405" to OpenApiSpecResponse(
|
||||
405 to OpenApiSpecResponse(
|
||||
description = "Validation exception",
|
||||
content = emptyMap()
|
||||
)
|
||||
@ -129,16 +130,16 @@ object TestData {
|
||||
requestBody = OpenApiSpecRequest(
|
||||
description = "Pet object that needs to be added to the store",
|
||||
content = mapOf(
|
||||
"application/json" to OpenApiSpecMediaType(
|
||||
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
|
||||
"application/json" to OpenApiSpecMediaType.Referenced(
|
||||
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
|
||||
),
|
||||
"application/xml" to OpenApiSpecMediaType(
|
||||
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
|
||||
"application/xml" to OpenApiSpecMediaType.Referenced(
|
||||
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = mapOf(
|
||||
"405" to OpenApiSpecResponse(
|
||||
405 to OpenApiSpecResponse(
|
||||
description = "Invalid Input",
|
||||
content = emptyMap()
|
||||
)
|
||||
@ -174,22 +175,22 @@ object TestData {
|
||||
)
|
||||
),
|
||||
responses = mapOf(
|
||||
"200" to OpenApiSpecResponse(
|
||||
200 to OpenApiSpecResponse(
|
||||
description = "successful operation",
|
||||
content = mapOf(
|
||||
"application/xml" to OpenApiSpecMediaType(
|
||||
"application/xml" to OpenApiSpecMediaType.Explicit(
|
||||
schema = OpenApiSpecSchemaArray(
|
||||
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
|
||||
)
|
||||
),
|
||||
"application/json" to OpenApiSpecMediaType(
|
||||
"application/json" to OpenApiSpecMediaType.Explicit(
|
||||
schema = OpenApiSpecSchemaArray(
|
||||
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
"400" to OpenApiSpecResponse(
|
||||
400 to OpenApiSpecResponse(
|
||||
description = "Invalid status value",
|
||||
content = mapOf()
|
||||
)
|
||||
|
@ -0,0 +1,26 @@
|
||||
package org.leafygreens.kompendium.util
|
||||
|
||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||
import org.leafygreens.kompendium.annotations.KompendiumRequest
|
||||
import org.leafygreens.kompendium.annotations.KompendiumResponse
|
||||
|
||||
data class TestParams(val a: String, val aa: Int)
|
||||
|
||||
data class TestNested(val nesty: String)
|
||||
|
||||
@KompendiumRequest("Example Request")
|
||||
data class TestRequest(
|
||||
@KompendiumField(name = "field_name")
|
||||
val fieldName: TestNested,
|
||||
val b: Double,
|
||||
val aaa: List<Long>
|
||||
)
|
||||
|
||||
@KompendiumResponse(200, "A Successful Endeavor")
|
||||
data class TestResponse(val c: String)
|
||||
|
||||
@KompendiumResponse(201, "Created Successfully")
|
||||
data class TestCreatedResponse(val id: Int, val c: String)
|
||||
|
||||
@KompendiumResponse(status = 204, "Entity was deleted successfully")
|
||||
object TestDeleteResponse
|
58
kompendium-core/src/test/resources/notarized_delete.json
Normal file
58
kompendium-core/src/test/resources/notarized_delete.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"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" : {
|
||||
"delete" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Test delete endpoint",
|
||||
"description" : "testing my deletes",
|
||||
"responses" : {
|
||||
"204" : {
|
||||
"description" : "Entity was deleted successfully",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestDeleteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"TestDeleteResponse" : {
|
||||
"properties" : { },
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
62
kompendium-core/src/test/resources/notarized_get.json
Normal file
62
kompendium-core/src/test/resources/notarized_get.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"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/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"TestResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
101
kompendium-core/src/test/resources/notarized_post.json
Normal file
101
kompendium-core/src/test/resources/notarized_post.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"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" : {
|
||||
"post" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Test post endpoint",
|
||||
"description" : "Post your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "Example Request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : false
|
||||
},
|
||||
"responses" : {
|
||||
"201" : {
|
||||
"description" : "Created Successfully",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestCreatedResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"TestCreatedResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"id" : {
|
||||
"format" : "int32",
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"TestRequest" : {
|
||||
"properties" : {
|
||||
"aaa" : {
|
||||
"items" : {
|
||||
"format" : "int64",
|
||||
"type" : "integer"
|
||||
},
|
||||
"type" : "array"
|
||||
},
|
||||
"b" : {
|
||||
"format" : "double",
|
||||
"type" : "number"
|
||||
},
|
||||
"field_name" : {
|
||||
"properties" : {
|
||||
"nesty" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
101
kompendium-core/src/test/resources/notarized_put.json
Normal file
101
kompendium-core/src/test/resources/notarized_put.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"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" : {
|
||||
"put" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Test put endpoint",
|
||||
"description" : "Put your tests here!",
|
||||
"requestBody" : {
|
||||
"description" : "Example Request",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : false
|
||||
},
|
||||
"responses" : {
|
||||
"201" : {
|
||||
"description" : "Created Successfully",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestCreatedResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"TestCreatedResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"id" : {
|
||||
"format" : "int32",
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"TestRequest" : {
|
||||
"properties" : {
|
||||
"aaa" : {
|
||||
"items" : {
|
||||
"format" : "int64",
|
||||
"type" : "integer"
|
||||
},
|
||||
"type" : "array"
|
||||
},
|
||||
"b" : {
|
||||
"format" : "double",
|
||||
"type" : "number"
|
||||
},
|
||||
"field_name" : {
|
||||
"properties" : {
|
||||
"nesty" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
62
kompendium-core/src/test/resources/path_parser.json
Normal file
62
kompendium-core/src/test/resources/path_parser.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"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" : {
|
||||
"/this/is/a/complex/path/with/an/{id}" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Another get test",
|
||||
"description" : "testing more",
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"TestResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
Reference in New Issue
Block a user