diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt index bd86e643e..4b6c1c792 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/plugin/NotarizedRoute.kt @@ -11,8 +11,10 @@ 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.util.Helpers.addToSpec import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug 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.definition.ReferenceDefinition import io.bkbn.kompendium.oas.OpenApiSpec @@ -31,17 +33,17 @@ import kotlin.reflect.KType object NotarizedRoute { - class Config { - var tags: Set = emptySet() - var parameters: List = emptyList() - var get: GetInfo? = null - var post: PostInfo? = null - var put: PutInfo? = null - var delete: DeleteInfo? = null - var patch: PatchInfo? = null - var head: HeadInfo? = null - var options: OptionsInfo? = null - var security: Map>? = null + class Config : SpecConfig { + override var tags: Set = emptySet() + override var parameters: List = 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>? = null internal var path: Path? = null } @@ -86,86 +88,5 @@ object NotarizedRoute { 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.toResponseMap(): Map = associate { error -> - error.responseCode.value to Response( - description = error.description, - content = error.responseType.toReferenceContent(error.examples) - ) - } - - private fun KType.toReferenceContent(examples: Map?): Map? = - 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("/\\(.+\\)"), "") } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt index 56826efbe..897fe3b0c 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt @@ -1,13 +1,26 @@ 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.KProperty 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 { @@ -32,4 +45,85 @@ object Helpers { .map { it.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.toResponseMap(): Map = associate { error -> + error.responseCode.value to Response( + description = error.description, + content = error.responseType.toReferenceContent(error.examples) + ) + } + + private fun KType.toReferenceContent(examples: Map?): Map? = + when (this.classifier as KClass<*>) { + Unit::class -> null + else -> mapOf( + "application/json" to MediaType( + schema = ReferenceDefinition(this.getReferenceSlug()), + examples = examples + ) + ) + } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/util/SpecConfig.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/util/SpecConfig.kt new file mode 100644 index 000000000..f0e2c1d2e --- /dev/null +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/util/SpecConfig.kt @@ -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 + var parameters: List + var get: GetInfo? + var post: PostInfo? + var put: PutInfo? + var delete: DeleteInfo? + var patch: PatchInfo? + var head: HeadInfo? + var options: OptionsInfo? + var security: Map>? +} diff --git a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt index 3e84d25b0..7df68f93d 100644 --- a/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt +++ b/core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt @@ -23,6 +23,7 @@ import io.ktor.http.HttpStatusCode import io.ktor.serialization.gson.gson import io.ktor.serialization.jackson.jackson import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.routing.Routing import io.ktor.server.testing.ApplicationTestBuilder @@ -63,17 +64,19 @@ object TestHelpers { fun openApiTestAllSerializers( snapshotName: String, customTypes: Map = emptyMap(), + applicationSetup: Application.() -> Unit = { }, routeUnderTest: Routing.() -> Unit ) { - openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, customTypes) - openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, customTypes) - openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, customTypes) + openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, customTypes) + openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, customTypes) + openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, customTypes) } private fun openApiTest( snapshotName: String, serializer: SupportedSerializer, routeUnderTest: Routing.() -> Unit, + applicationSetup: Application.() -> Unit, typeOverrides: Map = emptyMap() ) = testApplication { install(NotarizedApplication()) { @@ -95,6 +98,7 @@ object TestHelpers { } } } + application(applicationSetup) routing { redoc() routeUnderTest() diff --git a/locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt b/locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt deleted file mode 100644 index 8c024471d..000000000 --- a/locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt +++ /dev/null @@ -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 { -// val clazzList = determineLocationParents(classifier!!) -// return clazzList.associateWith { it.memberProperties } -// .flatMap { (clazz, memberProperties) -> memberProperties.associateWith { clazz }.toList() } -// .filter { (prop, _) -> prop.hasAnnotation() } -// .map { (prop, clazz) -> prop.toParameter(info, clazz.createType(), clazz, feature) } -// } -// -// private fun determineLocationParents(classifier: KClassifier): List> { -// var clazz: KClass<*>? = classifier as KClass<*> -// val clazzList = mutableListOf>() -// 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() } -// return parent?.returnType?.classifier as? KClass<*> -// } -// -// fun KClass<*>.calculateLocationPath(suffix: String = ""): String { -// val locationAnnotation = this.findAnnotation() -// require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } -// val parent = this.java.declaringClass?.kotlin?.takeIf { it.hasAnnotation() } -// val newSuffix = locationAnnotation.path.plus(suffix) -// return when (parent) { -// null -> newSuffix -// else -> parent.calculateLocationPath(newSuffix) -// } -// } -// -// inline fun processBaseInfo( -// paramType: KType, -// requestType: KType, -// responseType: KType, -// info: MethodInfo<*, *>, -// route: Route -// ): LocationBaseInfo { -// val locationAnnotation = TParam::class.findAnnotation() -// 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 -// ) -//} diff --git a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocation.kt b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocation.kt deleted file mode 100644 index d9d8f260a..000000000 --- a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocation.kt +++ /dev/null @@ -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 Route.notarizedGet( -// info: GetInfo, -// postProcess: (PathOperation) -> PathOperation = { p -> p }, -// noinline body: suspend PipelineContext.(TParam) -> Unit -// ): Route = methodNotarizationPreFlight() { paramType, requestType, responseType -> -// val lbi = LocationMethodParser.processBaseInfo(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 Route.notarizedPost( -// info: PostInfo, -// postProcess: (PathOperation) -> PathOperation = { p -> p }, -// noinline body: suspend PipelineContext.(TParam) -> Unit -// ): Route = methodNotarizationPreFlight() { paramType, requestType, responseType -> -// val lbi = LocationMethodParser.processBaseInfo(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 Route.notarizedPut( -// info: PutInfo, -// postProcess: (PathOperation) -> PathOperation = { p -> p }, -// noinline body: suspend PipelineContext.(TParam) -> Unit -// ): Route = methodNotarizationPreFlight() { paramType, requestType, responseType -> -// val lbi = LocationMethodParser.processBaseInfo(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 Route.notarizedDelete( -// info: DeleteInfo, -// postProcess: (PathOperation) -> PathOperation = { p -> p }, -// noinline body: suspend PipelineContext.(TParam) -> Unit -// ): Route = methodNotarizationPreFlight { paramType, requestType, responseType -> -// val lbi = LocationMethodParser.processBaseInfo(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) } -// } -// } -//} diff --git a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt new file mode 100644 index 000000000..8ee88d020 --- /dev/null +++ b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt @@ -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 = emptySet(), + override var parameters: List = 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>? = null, + ) : SpecConfig + + class Config { + lateinit var locations: Map, 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() + ?: 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() } + + return if (parent == null) { + path + } else { + parent.getLocationFromClass() + path + } + } +} diff --git a/locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt b/locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt index b54f18c85..e8ec0e2d5 100644 --- a/locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt +++ b/locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt @@ -1,82 +1,119 @@ package io.bkbn.kompendium.locations -//import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers -//import io.bkbn.kompendium.locations.util.locationsConfig -//import io.bkbn.kompendium.locations.util.notarizedDeleteNestedLocation -//import io.bkbn.kompendium.locations.util.notarizedDeleteSimpleLocation -//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocation -//import io.bkbn.kompendium.locations.util.notarizedGetNestedLocationFromNonLocationClass -//import io.bkbn.kompendium.locations.util.notarizedGetSimpleLocation -//import io.bkbn.kompendium.locations.util.notarizedPostNestedLocation -//import io.bkbn.kompendium.locations.util.notarizedPostSimpleLocation -//import io.bkbn.kompendium.locations.util.notarizedPutNestedLocation -//import io.bkbn.kompendium.locations.util.notarizedPutSimpleLocation -//import io.kotest.core.spec.style.DescribeSpec -// -//class KompendiumLocationsTest : DescribeSpec({ -// describe("Locations") { -// it("Can notarize a get request with a simple location") { -// // act -// openApiTestAllSerializers("notarized_get_simple_location.json") { -// locationsConfig() -// notarizedGetSimpleLocation() -// } -// } -// it("Can notarize a get request with a nested location") { -// // act -// openApiTestAllSerializers("notarized_get_nested_location.json") { -// locationsConfig() -// notarizedGetNestedLocation() -// } -// } -// it("Can notarize a post with a simple location") { -// // act -// openApiTestAllSerializers("notarized_post_simple_location.json") { -// locationsConfig() -// notarizedPostSimpleLocation() -// } -// } -// it("Can notarize a post with a nested location") { -// // act -// openApiTestAllSerializers("notarized_post_nested_location.json") { -// locationsConfig() -// notarizedPostNestedLocation() -// } -// } -// 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") { -// // act -// openApiTestAllSerializers("notarized_put_nested_location.json") { -// locationsConfig() -// notarizedPutNestedLocation() -// } -// } -// it("Can notarize a delete with a simple location") { -// // act -// openApiTestAllSerializers("notarized_delete_simple_location.json") { -// locationsConfig() -// notarizedDeleteSimpleLocation() -// } -// } -// it("Can notarize a delete with a nested location") { -// // act -// openApiTestAllSerializers("notarized_delete_nested_location.json") { -// locationsConfig() -// notarizedDeleteNestedLocation() -// } -// } -// it("Can notarize a get with a nested location nested in a non-location class") { -// // act -// openApiTestAllSerializers("notarized_get_nested_location_from_non_location_class.json") { -// locationsConfig() -// notarizedGetNestedLocationFromNonLocationClass() -// } -// } -// } -//}) +import Listing +import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.kotest.core.spec.style.DescribeSpec +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.locations.Locations +import io.ktor.server.locations.get +import io.ktor.server.response.respondText + +class KompendiumLocationsTest : DescribeSpec({ + describe("Location Tests") { + it("Can notarize a simple location") { + openApiTestAllSerializers( + snapshotName = "T0001__simple_location.json", + applicationSetup = { + install(Locations) + 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("Location") + description("example location") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("does great things") + } + } + ), + ) + } + } + ) { + get { listing -> + call.respondText("Listing ${listing.name}, page ${listing.page}") + } + } + } + it("Can notarize nested locations") { + openApiTestAllSerializers( + snapshotName = "T0002__nested_locations.json", + applicationSetup = { + install(Locations) + install(NotarizedLocations()) { + locations = mapOf( + Type.Edit::class to NotarizedLocations.LocationMetadata( + parameters = listOf( + Parameter( + name = "name", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ), + get = GetInfo.builder { + summary("Edit") + description("example location") + response { + responseCode(HttpStatusCode.OK) + responseType() + 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() + description("does great things") + } + } + ), + ) + } + } + ) { + get { edit -> + call.respondText("Listing ${edit.parent.name}") + } + get { other -> + call.respondText("Listing ${other.parent.name}, page ${other.page}") + } + } + } + } +}) diff --git a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt b/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt index 60316b711..be647ca12 100644 --- a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt +++ b/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt @@ -1,22 +1,12 @@ -package io.bkbn.kompendium.locations.util +import io.ktor.server.locations.Location -//import io.bkbn.kompendium.annotations.Param -//import io.bkbn.kompendium.annotations.ParamType -//import io.ktor.locations.Location -// -//@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) -//} -// -//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) +@Location("/list/{name}/page/{page}") +data class Listing(val name: String, val page: Int) + +@Location("/type/{name}") +data class Type(val name: String) { + @Location("/edit") + data class Edit(val parent: Type) + @Location("/other/{page}") + data class Other(val parent: Type, val page: Int) +} diff --git a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt b/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt deleted file mode 100644 index 94bc7292c..000000000 --- a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt +++ /dev/null @@ -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" } -// } -// } -// } -//} diff --git a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt b/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt deleted file mode 100644 index 7995687b6..000000000 --- a/locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt +++ /dev/null @@ -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( -// summary = "Location Test", -// description = "A cool test", -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testPostSimpleLocation = PostInfo( -// summary = "Location Test", -// description = "A cool test", -// requestInfo = RequestInfo( -// description = "Cool stuff" -// ), -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testPutSimpleLocation = PutInfo( -// summary = "Location Test", -// description = "A cool test", -// requestInfo = RequestInfo( -// description = "Cool stuff" -// ), -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testDeleteSimpleLocation = DeleteInfo( -// summary = "Location Test", -// description = "A cool test", -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testGetNestedLocation = GetInfo( -// summary = "Location Test", -// description = "A cool test", -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testPostNestedLocation = PostInfo( -// summary = "Location Test", -// description = "A cool test", -// requestInfo = RequestInfo( -// description = "Cool stuff" -// ), -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testPutNestedLocation = PutInfo( -// summary = "Location Test", -// description = "A cool test", -// requestInfo = RequestInfo( -// description = "Cool stuff" -// ), -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// val testDeleteNestedLocation = DeleteInfo( -// summary = "Location Test", -// description = "A cool test", -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -// -// val testGetNestedLocationFromNonLocationClass = GetInfo( -// summary = "Location Test", -// description = "A cool test", -// responseInfo = ResponseInfo( -// status = HttpStatusCode.OK, -// description = "A successful endeavor" -// ) -// ) -//} diff --git a/locations/src/test/resources/notarized_get_nested_location_from_non_location_class.json b/locations/src/test/resources/T0001__simple_location.json similarity index 55% rename from locations/src/test/resources/notarized_get_nested_location_from_non_location_class.json rename to locations/src/test/resources/T0001__simple_location.json index 4a9adabcc..fbd638a61 100644 --- a/locations/src/test/resources/notarized_get_nested_location_from_non_location_class.json +++ b/locations/src/test/resources/T0001__simple_location.json @@ -1,5 +1,6 @@ { - "openapi": "3.0.3", + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "Test API", "version": "1.33.7", @@ -26,59 +27,62 @@ } ], "paths": { - "/test/test/{name}/nesty": { + "/list/{name}/page/{page}": { "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 - } - ], + "summary": "Location", + "description": "example location", + "parameters": [], "responses": { "200": { - "description": "A successful endeavor", + "description": "does great things", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SimpleResponse" + "$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": { - "SimpleResponse": { + "TestResponse": { + "type": "object", "properties": { - "result": { - "type": "boolean" + "c": { + "type": "string" } }, "required": [ - "result" - ], - "type": "object" + "c" + ] } }, "securitySchemes": {} diff --git a/locations/src/test/resources/T0002__nested_locations.json b/locations/src/test/resources/T0002__nested_locations.json new file mode 100644 index 000000000..68c84bfcc --- /dev/null +++ b/locations/src/test/resources/T0002__nested_locations.json @@ -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": [] +} diff --git a/locations/src/test/resources/notarized_delete_nested_location.json b/locations/src/test/resources/notarized_delete_nested_location.json deleted file mode 100644 index 3e1149c76..000000000 --- a/locations/src/test/resources/notarized_delete_nested_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_delete_simple_location.json b/locations/src/test/resources/notarized_delete_simple_location.json deleted file mode 100644 index bfc884be6..000000000 --- a/locations/src/test/resources/notarized_delete_simple_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_get_nested_location.json b/locations/src/test/resources/notarized_get_nested_location.json deleted file mode 100644 index 4a9adabcc..000000000 --- a/locations/src/test/resources/notarized_get_nested_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_get_simple_location.json b/locations/src/test/resources/notarized_get_simple_location.json deleted file mode 100644 index 6be4779f5..000000000 --- a/locations/src/test/resources/notarized_get_simple_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_post_nested_location.json b/locations/src/test/resources/notarized_post_nested_location.json deleted file mode 100644 index 7f182e7a2..000000000 --- a/locations/src/test/resources/notarized_post_nested_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_post_simple_location.json b/locations/src/test/resources/notarized_post_simple_location.json deleted file mode 100644 index 6d0a2b8ee..000000000 --- a/locations/src/test/resources/notarized_post_simple_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_put_nested_location.json b/locations/src/test/resources/notarized_put_nested_location.json deleted file mode 100644 index 49f0a29b0..000000000 --- a/locations/src/test/resources/notarized_put_nested_location.json +++ /dev/null @@ -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": [] -} diff --git a/locations/src/test/resources/notarized_put_simple_location.json b/locations/src/test/resources/notarized_put_simple_location.json deleted file mode 100644 index c7e758620..000000000 --- a/locations/src/test/resources/notarized_put_simple_location.json +++ /dev/null @@ -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": [] -} diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt index c2f6fbcea..94bd2fe62 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt @@ -71,7 +71,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") authenticate("basic") { route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } @@ -80,7 +80,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt index 5ca2b0a31..1f384d7ae 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt @@ -47,7 +47,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } @@ -55,7 +55,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt index b0d98383b..5313230d8 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/CustomTypePlayground.kt @@ -54,7 +54,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond( HttpStatusCode.OK, @@ -68,7 +68,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt index a31a18f6a..95876f4ec 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt @@ -13,7 +13,6 @@ 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.netty.Netty @@ -55,7 +54,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { 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()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt index 2ca8c4573..d6e8d6aa4 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/GsonSerializationPlayground.kt @@ -43,7 +43,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } @@ -51,7 +51,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt index 6fce2099a..c3db6095d 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt @@ -82,7 +82,7 @@ private fun Application.mainModule() { authenticate("basic") { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } @@ -91,7 +91,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt index a3dc8ebcb..612b4e19c 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/JacksonSerializationPlayground.kt @@ -46,7 +46,7 @@ private fun Application.mainModule() { redoc(pageTitle = "Simple API Docs") route("/{id}") { - idDocumentation() + locationDocumentation() get { call.respond(HttpStatusCode.OK, ExampleResponse(true)) } @@ -54,7 +54,7 @@ private fun Application.mainModule() { } } -private fun Route.idDocumentation() { +private fun Route.locationDocumentation() { install(NotarizedRoute()) { parameters = listOf( Parameter( diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt new file mode 100644 index 000000000..a715ee4b9 --- /dev/null +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt @@ -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() + description("Will return whether or not the user is real 😱") + } + } + ), + ) + } + routing { + redoc(pageTitle = "Simple API Docs") + get { listing -> + call.respondText("Listing ${listing.name}, page ${listing.page}") + } + } +} diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Models.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Models.kt index a38a87eeb..f1c42e24e 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Models.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Models.kt @@ -1,5 +1,7 @@ package io.bkbn.kompendium.playground.util +import io.ktor.server.locations.KtorExperimentalLocationsAPI +import io.ktor.server.locations.Location import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -14,3 +16,12 @@ data class CustomTypeResponse( @Serializable 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) +}