feat: v3 locations (#292)

This commit is contained in:
Ryan Brink
2022-08-13 12:40:20 -07:00
committed by GitHub
parent 2007b1bb8c
commit 62080f3248
30 changed files with 616 additions and 1404 deletions

View File

@ -11,8 +11,10 @@ import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.json.schema.SchemaGenerator import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
@ -31,17 +33,17 @@ import kotlin.reflect.KType
object NotarizedRoute { object NotarizedRoute {
class Config { class Config : SpecConfig {
var tags: Set<String> = emptySet() override var tags: Set<String> = emptySet()
var parameters: List<Parameter> = emptyList() override var parameters: List<Parameter> = emptyList()
var get: GetInfo? = null override var get: GetInfo? = null
var post: PostInfo? = null override var post: PostInfo? = null
var put: PutInfo? = null override var put: PutInfo? = null
var delete: DeleteInfo? = null override var delete: DeleteInfo? = null
var patch: PatchInfo? = null override var patch: PatchInfo? = null
var head: HeadInfo? = null override var head: HeadInfo? = null
var options: OptionsInfo? = null override var options: OptionsInfo? = null
var security: Map<String, List<String>>? = null override var security: Map<String, List<String>>? = null
internal var path: Path? = null internal var path: Path? = null
} }
@ -86,86 +88,5 @@ object NotarizedRoute {
pluginConfig.path = path pluginConfig.path = path
} }
private fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: Config) {
SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
}
errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[error.responseType.getSimpleSlug()] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
}
}
else -> {}
}
val operations = this.toPathOperation(config)
when (this) {
is DeleteInfo -> path.delete = operations
is GetInfo -> path.get = operations
is HeadInfo -> path.head = operations
is PatchInfo -> path.patch = operations
is PostInfo -> path.post = operations
is PutInfo -> path.put = operations
is OptionsInfo -> path.options = operations
}
}
private fun MethodInfo.toPathOperation(config: Config) = PathOperation(
tags = config.tags.plus(this.tags),
summary = this.summary,
description = this.description,
externalDocs = this.externalDocumentation,
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toList(),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
required = true
)
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
)
).plus(this.errors.toResponseMap())
)
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
)
}
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
schema = ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
}
private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "") private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
} }

View File

@ -1,13 +1,26 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.metadata.MethodInfoWithRequest
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory
import java.lang.reflect.ParameterizedType
import java.util.Locale
object Helpers { object Helpers {
@ -32,4 +45,85 @@ object Helpers {
.map { it.simpleName } .map { it.simpleName }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-") return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
} }
fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig) {
SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
}
errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[error.responseType.getSimpleSlug()] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
}
}
else -> {}
}
val operations = this.toPathOperation(config)
when (this) {
is DeleteInfo -> path.delete = operations
is GetInfo -> path.get = operations
is HeadInfo -> path.head = operations
is PatchInfo -> path.patch = operations
is PostInfo -> path.post = operations
is PutInfo -> path.put = operations
is OptionsInfo -> path.options = operations
}
}
private fun MethodInfo.toPathOperation(config: SpecConfig) = PathOperation(
tags = config.tags.plus(this.tags),
summary = this.summary,
description = this.description,
externalDocs = this.externalDocumentation,
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toList(),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
required = true
)
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
)
).plus(this.errors.toResponseMap())
)
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
)
}
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
schema = ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
}
} }

View File

@ -0,0 +1,23 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.oas.payload.Parameter
interface SpecConfig {
var tags: Set<String>
var parameters: List<Parameter>
var get: GetInfo?
var post: PostInfo?
var put: PutInfo?
var delete: DeleteInfo?
var patch: PatchInfo?
var head: HeadInfo?
var options: OptionsInfo?
var security: Map<String, List<String>>?
}

View File

@ -23,6 +23,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson import io.ktor.serialization.jackson.jackson
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.ApplicationTestBuilder
@ -63,17 +64,19 @@ object TestHelpers {
fun openApiTestAllSerializers( fun openApiTestAllSerializers(
snapshotName: String, snapshotName: String,
customTypes: Map<KType, JsonSchema> = emptyMap(), customTypes: Map<KType, JsonSchema> = emptyMap(),
applicationSetup: Application.() -> Unit = { },
routeUnderTest: Routing.() -> Unit routeUnderTest: Routing.() -> Unit
) { ) {
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, customTypes)
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, customTypes)
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, customTypes) openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, customTypes)
} }
private fun openApiTest( private fun openApiTest(
snapshotName: String, snapshotName: String,
serializer: SupportedSerializer, serializer: SupportedSerializer,
routeUnderTest: Routing.() -> Unit, routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit,
typeOverrides: Map<KType, JsonSchema> = emptyMap() typeOverrides: Map<KType, JsonSchema> = emptyMap()
) = testApplication { ) = testApplication {
install(NotarizedApplication()) { install(NotarizedApplication()) {
@ -95,6 +98,7 @@ object TestHelpers {
} }
} }
} }
application(applicationSetup)
routing { routing {
redoc() redoc()
routeUnderTest() routeUnderTest()

View File

@ -1,84 +0,0 @@
package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.annotations.Param
//import io.bkbn.kompendium.core.Kompendium
//import io.bkbn.kompendium.core.metadata.method.MethodInfo
//import io.bkbn.kompendium.core.parser.IMethodParser
//import io.bkbn.kompendium.oas.path.Path
//import io.bkbn.kompendium.oas.path.PathOperation
//import io.bkbn.kompendium.oas.payload.Parameter
//import io.ktor.application.feature
//import io.ktor.locations.KtorExperimentalLocationsAPI
//import io.ktor.locations.Location
//import io.ktor.routing.Route
//import io.ktor.routing.application
//import kotlin.reflect.KAnnotatedElement
//import kotlin.reflect.KClass
//import kotlin.reflect.KClassifier
//import kotlin.reflect.KType
//import kotlin.reflect.full.createType
//import kotlin.reflect.full.findAnnotation
//import kotlin.reflect.full.hasAnnotation
//import kotlin.reflect.full.memberProperties
//
//@OptIn(KtorExperimentalLocationsAPI::class)
//object LocationMethodParser : IMethodParser {
// override fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
// val clazzList = determineLocationParents(classifier!!)
// return clazzList.associateWith { it.memberProperties }
// .flatMap { (clazz, memberProperties) -> memberProperties.associateWith { clazz }.toList() }
// .filter { (prop, _) -> prop.hasAnnotation<Param>() }
// .map { (prop, clazz) -> prop.toParameter(info, clazz.createType(), clazz, feature) }
// }
//
// private fun determineLocationParents(classifier: KClassifier): List<KClass<*>> {
// var clazz: KClass<*>? = classifier as KClass<*>
// val clazzList = mutableListOf<KClass<*>>()
// while (clazz != null) {
// clazzList.add(clazz)
// clazz = getLocationParent(clazz)
// }
// return clazzList
// }
//
// private fun getLocationParent(clazz: KClass<*>): KClass<*>? {
// val parent = clazz.memberProperties
// .find { (it.returnType.classifier as KAnnotatedElement).hasAnnotation<Location>() }
// return parent?.returnType?.classifier as? KClass<*>
// }
//
// fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
// val locationAnnotation = this.findAnnotation<Location>()
// require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
// val parent = this.java.declaringClass?.kotlin?.takeIf { it.hasAnnotation<Location>() }
// val newSuffix = locationAnnotation.path.plus(suffix)
// return when (parent) {
// null -> newSuffix
// else -> parent.calculateLocationPath(newSuffix)
// }
// }
//
// inline fun <reified TParam : Any> processBaseInfo(
// paramType: KType,
// requestType: KType,
// responseType: KType,
// info: MethodInfo<*, *>,
// route: Route
// ): LocationBaseInfo {
// val locationAnnotation = TParam::class.findAnnotation<Location>()
// require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
// val path = route.calculateRoutePath()
// val locationPath = TParam::class.calculateLocationPath()
// val pathWithLocation = path.plus(locationPath)
// val feature = route.application.feature(Kompendium)
// feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
// val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
// return LocationBaseInfo(baseInfo, feature, pathWithLocation)
// }
//
// data class LocationBaseInfo(
// val op: PathOperation,
// val feature: Kompendium,
// val path: String
// )
//}

View File

@ -1,110 +0,0 @@
package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
//import io.bkbn.kompendium.core.metadata.method.DeleteInfo
//import io.bkbn.kompendium.core.metadata.method.GetInfo
//import io.bkbn.kompendium.core.metadata.method.PostInfo
//import io.bkbn.kompendium.core.metadata.method.PutInfo
//import io.bkbn.kompendium.oas.path.PathOperation
//import io.ktor.application.ApplicationCall
//import io.ktor.http.HttpMethod
//import io.ktor.locations.KtorExperimentalLocationsAPI
//import io.ktor.locations.handle
//import io.ktor.locations.location
//import io.ktor.routing.Route
//import io.ktor.routing.method
//import io.ktor.util.pipeline.PipelineContext
//
///**
// * This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access
// * to all path and query parameters.
// */
//@KtorExperimentalLocationsAPI
//object NotarizedLocation {
//
// /**
// * Notarization for an HTTP GET request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param].
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
// info: GetInfo<TParam, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.get = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Get) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TReq Class detailing the expected API request body
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
// info: PostInfo<TParam, TReq, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.post = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Post) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP Delete request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TReq Class detailing the expected API request body
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
// info: PutInfo<TParam, TReq, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.put = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Put) { handle(body) }
// }
// }
//
// /**
// * Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
// * @param TParam The class containing all parameter fields.
// * Each field must be annotated with @[io.bkbn.kompendium.annotations.Param]
// * Additionally, the class must be annotated with @[io.ktor.locations.Location].
// * @param TResp Class detailing the expected API response
// * @param info Route metadata
// * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
// */
// inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
// info: DeleteInfo<TParam, TResp>,
// postProcess: (PathOperation) -> PathOperation = { p -> p },
// noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
// ): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
// val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
// lbi.feature.config.spec.paths[lbi.path]?.delete = postProcess(lbi.op)
// return location(TParam::class) {
// method(HttpMethod.Delete) { handle(body) }
// }
// }
//}

View File

@ -0,0 +1,79 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.locations.KtorExperimentalLocationsAPI
import io.ktor.server.locations.Location
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
object NotarizedLocations {
data class LocationMetadata(
override var tags: Set<String> = emptySet(),
override var parameters: List<Parameter> = emptyList(),
override var get: GetInfo? = null,
override var post: PostInfo? = null,
override var put: PutInfo? = null,
override var delete: DeleteInfo? = null,
override var patch: PatchInfo? = null,
override var head: HeadInfo? = null,
override var options: OptionsInfo? = null,
override var security: Map<String, List<String>>? = null,
) : SpecConfig
class Config {
lateinit var locations: Map<KClass<*>, LocationMetadata>
}
operator fun invoke() = createApplicationPlugin(
name = "NotarizedLocations",
createConfiguration = ::Config
) {
val spec = application.attributes[KompendiumAttributes.openApiSpec]
pluginConfig.locations.forEach { (k, v) ->
val path = Path()
path.parameters = v.parameters
v.get?.addToSpec(path, spec, v)
v.delete?.addToSpec(path, spec, v)
v.head?.addToSpec(path, spec, v)
v.options?.addToSpec(path, spec, v)
v.post?.addToSpec(path, spec, v)
v.put?.addToSpec(path, spec, v)
v.patch?.addToSpec(path, spec, v)
val location = k.getLocationFromClass()
spec.paths[location] = path
}
}
@OptIn(KtorExperimentalLocationsAPI::class)
private fun KClass<*>.getLocationFromClass(): String {
// todo if parent
val location = findAnnotation<Location>()
?: error("Cannot notarize a location without annotating with @Location")
val path = location.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Location>() }
return if (parent == null) {
path
} else {
parent.getLocationFromClass() + path
}
}
}

View File

@ -1,82 +1,119 @@
package io.bkbn.kompendium.locations package io.bkbn.kompendium.locations
//import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import Listing
//import io.bkbn.kompendium.locations.util.locationsConfig import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
//import io.bkbn.kompendium.locations.util.notarizedDeleteNestedLocation import io.bkbn.kompendium.core.fixtures.TestResponse
//import io.bkbn.kompendium.locations.util.notarizedDeleteSimpleLocation import io.bkbn.kompendium.core.metadata.GetInfo
//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocation import io.bkbn.kompendium.json.schema.definition.TypeDefinition
//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocationFromNonLocationClass import io.bkbn.kompendium.oas.payload.Parameter
//import io.bkbn.kompendium.locations.util.notarizedGetSimpleLocation import io.kotest.core.spec.style.DescribeSpec
//import io.bkbn.kompendium.locations.util.notarizedPostNestedLocation import io.ktor.http.HttpStatusCode
//import io.bkbn.kompendium.locations.util.notarizedPostSimpleLocation import io.ktor.server.application.call
//import io.bkbn.kompendium.locations.util.notarizedPutNestedLocation import io.ktor.server.application.install
//import io.bkbn.kompendium.locations.util.notarizedPutSimpleLocation import io.ktor.server.locations.Locations
//import io.kotest.core.spec.style.DescribeSpec import io.ktor.server.locations.get
// import io.ktor.server.response.respondText
//class KompendiumLocationsTest : DescribeSpec({
// describe("Locations") { class KompendiumLocationsTest : DescribeSpec({
// it("Can notarize a get request with a simple location") { describe("Location Tests") {
// // act it("Can notarize a simple location") {
// openApiTestAllSerializers("notarized_get_simple_location.json") { openApiTestAllSerializers(
// locationsConfig() snapshotName = "T0001__simple_location.json",
// notarizedGetSimpleLocation() applicationSetup = {
// } install(Locations)
// } install(NotarizedLocations()) {
// it("Can notarize a get request with a nested location") { locations = mapOf(
// // act Listing::class to NotarizedLocations.LocationMetadata(
// openApiTestAllSerializers("notarized_get_nested_location.json") { parameters = listOf(
// locationsConfig() Parameter(
// notarizedGetNestedLocation() name = "name",
// } `in` = Parameter.Location.path,
// } schema = TypeDefinition.STRING
// it("Can notarize a post with a simple location") { ),
// // act Parameter(
// openApiTestAllSerializers("notarized_post_simple_location.json") { name = "page",
// locationsConfig() `in` = Parameter.Location.path,
// notarizedPostSimpleLocation() schema = TypeDefinition.INT
// } )
// } ),
// it("Can notarize a post with a nested location") { get = GetInfo.builder {
// // act summary("Location")
// openApiTestAllSerializers("notarized_post_nested_location.json") { description("example location")
// locationsConfig() response {
// notarizedPostNestedLocation() responseCode(HttpStatusCode.OK)
// } responseType<TestResponse>()
// } description("does great things")
// it("Can notarize a put with a simple location") { }
// // act }
// openApiTestAllSerializers("notarized_put_simple_location.json") { ),
// locationsConfig() )
// notarizedPutSimpleLocation() }
// } }
// } ) {
// it("Can notarize a put with a nested location") { get<Listing> { listing ->
// // act call.respondText("Listing ${listing.name}, page ${listing.page}")
// openApiTestAllSerializers("notarized_put_nested_location.json") { }
// locationsConfig() }
// notarizedPutNestedLocation() }
// } it("Can notarize nested locations") {
// } openApiTestAllSerializers(
// it("Can notarize a delete with a simple location") { snapshotName = "T0002__nested_locations.json",
// // act applicationSetup = {
// openApiTestAllSerializers("notarized_delete_simple_location.json") { install(Locations)
// locationsConfig() install(NotarizedLocations()) {
// notarizedDeleteSimpleLocation() locations = mapOf(
// } Type.Edit::class to NotarizedLocations.LocationMetadata(
// } parameters = listOf(
// it("Can notarize a delete with a nested location") { Parameter(
// // act name = "name",
// openApiTestAllSerializers("notarized_delete_nested_location.json") { `in` = Parameter.Location.path,
// locationsConfig() schema = TypeDefinition.STRING
// notarizedDeleteNestedLocation() )
// } ),
// } get = GetInfo.builder {
// it("Can notarize a get with a nested location nested in a non-location class") { summary("Edit")
// // act description("example location")
// openApiTestAllSerializers("notarized_get_nested_location_from_non_location_class.json") { response {
// locationsConfig() responseCode(HttpStatusCode.OK)
// notarizedGetNestedLocationFromNonLocationClass() responseType<TestResponse>()
// } description("does great things")
// } }
// } }
//}) ),
Type.Other::class to NotarizedLocations.LocationMetadata(
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
),
get = GetInfo.builder {
summary("Other")
description("example location")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
),
)
}
}
) {
get<Type.Edit> { edit ->
call.respondText("Listing ${edit.parent.name}")
}
get<Type.Other> { other ->
call.respondText("Listing ${other.parent.name}, page ${other.page}")
}
}
}
}
})

View File

@ -1,22 +1,12 @@
package io.bkbn.kompendium.locations.util import io.ktor.server.locations.Location
//import io.bkbn.kompendium.annotations.Param @Location("/list/{name}/page/{page}")
//import io.bkbn.kompendium.annotations.ParamType data class Listing(val name: String, val page: Int)
//import io.ktor.locations.Location
// @Location("/type/{name}")
//@Location("/test/{name}") data class Type(val name: String) {
//data class SimpleLoc(@Param(ParamType.PATH) val name: String) { @Location("/edit")
// @Location("/nesty") data class Edit(val parent: Type)
// data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc) @Location("/other/{page}")
//} data class Other(val parent: Type, val page: Int)
// }
//object NonLocationObject {
// @Location("/test/{name}")
// data class SimpleLoc(@Param(ParamType.PATH) val name: String) {
// @Location("/nesty")
// data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc)
// }
//}
//
//data class SimpleResponse(val result: Boolean)
//data class SimpleRequest(val input: String)

View File

@ -1,107 +0,0 @@
package io.bkbn.kompendium.locations.util
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedDelete
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedPost
//import io.bkbn.kompendium.locations.NotarizedLocation.notarizedPut
//import io.ktor.application.Application
//import io.ktor.application.call
//import io.ktor.application.install
//import io.ktor.locations.Locations
//import io.ktor.response.respondText
//import io.ktor.routing.route
//import io.ktor.routing.routing
//
//fun Application.locationsConfig() {
// install(Locations)
//}
//
//fun Application.notarizedGetSimpleLocation() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedGetNestedLocation() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPostSimpleLocation() {
// routing {
// route("/test") {
// notarizedPost(TestResponseInfo.testPostSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPostNestedLocation() {
// routing {
// route("/test") {
// notarizedPost(TestResponseInfo.testPostNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPutSimpleLocation() {
// routing {
// route("/test") {
// notarizedPut(TestResponseInfo.testPutSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedPutNestedLocation() {
// routing {
// route("/test") {
// notarizedPut(TestResponseInfo.testPutNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedDeleteSimpleLocation() {
// routing {
// route("/test") {
// notarizedDelete(TestResponseInfo.testDeleteSimpleLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedDeleteNestedLocation() {
// routing {
// route("/test") {
// notarizedDelete(TestResponseInfo.testDeleteNestedLocation) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}
//
//fun Application.notarizedGetNestedLocationFromNonLocationClass() {
// routing {
// route("/test") {
// notarizedGet(TestResponseInfo.testGetNestedLocationFromNonLocationClass) {
// call.respondText { "hey dude ‼️ congratz on the get request" }
// }
// }
// }
//}

View File

@ -1,97 +0,0 @@
package io.bkbn.kompendium.locations.util
//import io.bkbn.kompendium.core.legacy.metadata.RequestInfo
//import io.bkbn.kompendium.core.legacy.metadata.ResponseInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.DeleteInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.GetInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.PostInfo
//import io.bkbn.kompendium.core.legacy.metadata.method.PutInfo
//import io.ktor.http.HttpStatusCode
//
//object TestResponseInfo {
// val testGetSimpleLocation = GetInfo<SimpleLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPostSimpleLocation = PostInfo<SimpleLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPutSimpleLocation = PutInfo<SimpleLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testDeleteSimpleLocation = DeleteInfo<SimpleLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testGetNestedLocation = GetInfo<SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPostNestedLocation = PostInfo<SimpleLoc.NestedLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testPutNestedLocation = PutInfo<SimpleLoc.NestedLoc, SimpleRequest, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// requestInfo = RequestInfo(
// description = "Cool stuff"
// ),
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
// val testDeleteNestedLocation = DeleteInfo<SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
//
// val testGetNestedLocationFromNonLocationClass = GetInfo<NonLocationObject.SimpleLoc.NestedLoc, SimpleResponse>(
// summary = "Location Test",
// description = "A cool test",
// responseInfo = ResponseInfo(
// status = HttpStatusCode.OK,
// description = "A successful endeavor"
// )
// )
//}

View File

@ -1,5 +1,6 @@
{ {
"openapi": "3.0.3", "openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": { "info": {
"title": "Test API", "title": "Test API",
"version": "1.33.7", "version": "1.33.7",
@ -26,59 +27,62 @@
} }
], ],
"paths": { "paths": {
"/test/test/{name}/nesty": { "/list/{name}/page/{page}": {
"get": { "get": {
"tags": [], "tags": [],
"summary": "Location Test", "summary": "Location",
"description": "A cool test", "description": "example location",
"parameters": [ "parameters": [],
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": { "responses": {
"200": { "200": {
"description": "A successful endeavor", "description": "does great things",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/SimpleResponse" "$ref": "#/components/schemas/TestResponse"
} }
} }
} }
} }
}, },
"deprecated": false "deprecated": false
} },
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "page",
"in": "path",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
} }
}, },
"webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"SimpleResponse": { "TestResponse": {
"type": "object",
"properties": { "properties": {
"result": { "c": {
"type": "boolean" "type": "string"
} }
}, },
"required": [ "required": [
"result" "c"
], ]
"type": "object"
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -0,0 +1,124 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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": {
"/type/{name}/edit": {
"get": {
"tags": [],
"summary": "Edit",
"description": "example location",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
},
"/type/{name}/other/{page}": {
"get": {
"tags": [],
"summary": "Other",
"description": "example location",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "page",
"in": "path",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,88 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}/nesty": {
"delete": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,79 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}": {
"delete": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,88 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}/nesty": {
"get": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,79 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}": {
"get": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,110 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}/nesty": {
"post": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,101 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}": {
"post": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,110 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}/nesty": {
"put": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "isCool",
"in": "query",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,101 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 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/bkbnio/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/test/{name}": {
"put": {
"tags": [],
"summary": "Location Test",
"description": "A cool test",
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "Cool stuff",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleResponse"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"SimpleRequest": {
"properties": {
"input": {
"type": "string"
}
},
"required": [
"input"
],
"type": "object"
},
"SimpleResponse": {
"properties": {
"result": {
"type": "boolean"
}
},
"required": [
"result"
],
"type": "object"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -71,7 +71,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
authenticate("basic") { authenticate("basic") {
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -80,7 +80,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -47,7 +47,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -55,7 +55,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -54,7 +54,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
@ -68,7 +68,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -13,7 +13,6 @@ import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
@ -55,7 +54,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
throw RuntimeException("This wasn't your fault I promise <3") throw RuntimeException("This wasn't your fault I promise <3")
} }
@ -63,7 +62,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -43,7 +43,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -51,7 +51,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -82,7 +82,7 @@ private fun Application.mainModule() {
authenticate("basic") { authenticate("basic") {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -91,7 +91,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -46,7 +46,7 @@ private fun Application.mainModule() {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() locationDocumentation()
get { get {
call.respond(HttpStatusCode.OK, ExampleResponse(true)) call.respond(HttpStatusCode.OK, ExampleResponse(true))
} }
@ -54,7 +54,7 @@ private fun Application.mainModule() {
} }
} }
private fun Route.idDocumentation() { private fun Route.locationDocumentation() {
install(NotarizedRoute()) { install(NotarizedRoute()) {
parameters = listOf( parameters = listOf(
Parameter( Parameter(

View File

@ -0,0 +1,80 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.locations.NotarizedLocations
import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.util.ExampleResponse
import io.bkbn.kompendium.playground.util.Listing
import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.locations.Locations
import io.ktor.server.locations.get
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respondText
import io.ktor.server.routing.routing
import kotlinx.serialization.json.Json
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(Locations)
install(ContentNegotiation) {
json(Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
})
}
install(NotarizedApplication()) {
spec = baseSpec
}
install(NotarizedLocations()) {
locations = mapOf(
Listing::class to NotarizedLocations.LocationMetadata(
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
),
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
),
)
}
routing {
redoc(pageTitle = "Simple API Docs")
get<Listing> { listing ->
call.respondText("Listing ${listing.name}, page ${listing.page}")
}
}
}

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.playground.util package io.bkbn.kompendium.playground.util
import io.ktor.server.locations.KtorExperimentalLocationsAPI
import io.ktor.server.locations.Location
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -14,3 +16,12 @@ data class CustomTypeResponse(
@Serializable @Serializable
data class ExceptionResponse(val message: String) data class ExceptionResponse(val message: String)
@Location("/list/{name}/page/{page}")
data class Listing(val name: String, val page: Int)
@Location("/type/{name}") data class Type(val name: String) {
// In these classes we have to include the `name` property matching the parent.
@Location("/edit") data class Edit(val parent: Type)
@Location("/other/{page}") data class Other(val parent: Type, val page: Int)
}