diff --git a/CHANGELOG.md b/CHANGELOG.md index 550c43315..75d5485b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Changed +- Support registering same path with different authentication and methods + ### Remove --- 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 20de9a33b..67ae52ee2 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,7 +11,6 @@ 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.path.PathOperation import io.bkbn.kompendium.oas.payload.Parameter import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.Hook @@ -31,7 +30,6 @@ object NotarizedRoute { override var head: HeadInfo? = null override var options: OptionsInfo? = null override var security: Map>? = null - internal var path: Path? = null } private object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> { @@ -51,32 +49,22 @@ object NotarizedRoute { val spec = application.attributes[KompendiumAttributes.openApiSpec] val routePath = route.calculateRoutePath() val authMethods = route.collectAuthMethods() - pluginConfig.path?.addDefaultAuthMethods(authMethods) - require(spec.paths[routePath] == null) { - """ - The specified path ${Parameter.Location.path} has already been documented! - Please make sure that all notarized paths are unique - """.trimIndent() - } - spec.paths[routePath] = pluginConfig.path - ?: error("This indicates a bug in Kompendium. Please file a GitHub issue!") + + val path = spec.paths[routePath] ?: Path() + + path.parameters = path.parameters?.plus(pluginConfig.parameters) ?: pluginConfig.parameters + val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] + + pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods) + + spec.paths[routePath] = path } - - val spec = application.attributes[KompendiumAttributes.openApiSpec] - val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] - - val path = Path() - path.parameters = pluginConfig.parameters - - pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader) - pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader) - - pluginConfig.path = path } private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "") @@ -86,27 +74,4 @@ object NotarizedRoute { .map { it.replace("(authenticate ", "").replace(")", "") } .map { it.split(", ") } .flatten() - - private fun Path.addDefaultAuthMethods(methods: List) { - get?.addDefaultAuthMethods(methods) - put?.addDefaultAuthMethods(methods) - post?.addDefaultAuthMethods(methods) - delete?.addDefaultAuthMethods(methods) - options?.addDefaultAuthMethods(methods) - head?.addDefaultAuthMethods(methods) - patch?.addDefaultAuthMethods(methods) - trace?.addDefaultAuthMethods(methods) - } - - private fun PathOperation.addDefaultAuthMethods(methods: List) { - methods.forEach { m -> - if (security == null || security?.all { s -> !s.containsKey(m) } == true) { - if (security == null) { - security = mutableListOf(mapOf(m to emptyList())) - } else { - security?.add(mapOf(m to emptyList())) - } - } - } - } } 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 70b62d163..a2175abb4 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 @@ -24,11 +24,31 @@ 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.KMutableProperty1 import kotlin.reflect.KType object Helpers { - fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig, schemaConfigurator: SchemaConfigurator) { + private fun PathOperation.addDefaultAuthMethods(methods: List) { + methods.forEach { m -> + if (security == null || security?.all { s -> !s.containsKey(m) } == true) { + if (security == null) { + security = mutableListOf(mapOf(m to emptyList())) + } else { + security?.add(mapOf(m to emptyList())) + } + } + } + } + + fun MethodInfo.addToSpec( + path: Path, + spec: OpenApiSpec, + config: SpecConfig, + schemaConfigurator: SchemaConfigurator, + routePath: String, + authMethods: List = emptyList() + ) { SchemaGenerator.fromTypeOrUnit( this.response.responseType, spec.components.schemas, schemaConfigurator @@ -57,15 +77,25 @@ object Helpers { } val operations = this.toPathOperation(config) + operations.addDefaultAuthMethods(authMethods) - 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 + fun setOperation( + property: KMutableProperty1 + ) { + require(property.get(path) == null) { + "A route has already been registered for path: $routePath and method: ${property.name.uppercase()}" + } + property.set(path, operations) + } + + return when (this) { + is DeleteInfo -> setOperation(Path::delete) + is GetInfo -> setOperation(Path::get) + is HeadInfo -> setOperation(Path::head) + is PatchInfo -> setOperation(Path::patch) + is PostInfo -> setOperation(Path::post) + is PutInfo -> setOperation(Path::put) + is OptionsInfo -> setOperation(Path::options) } } diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 9025728b8..e3ed83a39 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -2,58 +2,60 @@ package io.bkbn.kompendium.core import dev.forst.ktor.apikey.apiKey import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers -import io.bkbn.kompendium.core.util.TestModules.complexRequest -import io.bkbn.kompendium.core.util.TestModules.customAuthConfig -import io.bkbn.kompendium.core.util.TestModules.customFieldNameResponse -import io.bkbn.kompendium.core.util.TestModules.dateTimeString -import io.bkbn.kompendium.core.util.TestModules.defaultAuthConfig -import io.bkbn.kompendium.core.util.TestModules.defaultField -import io.bkbn.kompendium.core.util.TestModules.defaultParameter -import io.bkbn.kompendium.core.util.TestModules.exampleParams -import io.bkbn.kompendium.core.util.TestModules.genericException -import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse -import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls -import io.bkbn.kompendium.core.util.TestModules.gnarlyGenericResponse -import io.bkbn.kompendium.core.util.TestModules.headerParameter -import io.bkbn.kompendium.core.util.TestModules.ignoredFieldsResponse -import io.bkbn.kompendium.core.util.TestModules.multipleAuthStrategies -import io.bkbn.kompendium.core.util.TestModules.multipleExceptions -import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection -import io.bkbn.kompendium.core.util.TestModules.nestedGenericMultipleParamsCollection -import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse -import io.bkbn.kompendium.core.util.TestModules.nestedTypeName -import io.bkbn.kompendium.core.util.TestModules.nestedUnderRoot -import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam -import io.bkbn.kompendium.core.util.TestModules.nonRequiredParams -import io.bkbn.kompendium.core.util.TestModules.notarizedDelete -import io.bkbn.kompendium.core.util.TestModules.notarizedGet -import io.bkbn.kompendium.core.util.TestModules.notarizedHead -import io.bkbn.kompendium.core.util.TestModules.notarizedOptions -import io.bkbn.kompendium.core.util.TestModules.notarizedPatch -import io.bkbn.kompendium.core.util.TestModules.notarizedPost -import io.bkbn.kompendium.core.util.TestModules.notarizedPut -import io.bkbn.kompendium.core.util.TestModules.nullableEnumField -import io.bkbn.kompendium.core.util.TestModules.nullableField -import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject -import io.bkbn.kompendium.core.util.TestModules.nullableReference -import io.bkbn.kompendium.core.util.TestModules.overrideMediaTypes -import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse -import io.bkbn.kompendium.core.util.TestModules.polymorphicException -import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse -import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse -import io.bkbn.kompendium.core.util.TestModules.primitives -import io.bkbn.kompendium.core.util.TestModules.reqRespExamples -import io.bkbn.kompendium.core.util.TestModules.requiredParams -import io.bkbn.kompendium.core.util.TestModules.returnsList -import io.bkbn.kompendium.core.util.TestModules.rootRoute -import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse -import io.bkbn.kompendium.core.util.TestModules.simplePathParsing -import io.bkbn.kompendium.core.util.TestModules.simpleRecursive -import io.bkbn.kompendium.core.util.TestModules.singleException -import io.bkbn.kompendium.core.util.TestModules.topLevelNullable -import io.bkbn.kompendium.core.util.TestModules.trailingSlash -import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse -import io.bkbn.kompendium.core.util.TestModules.withOperationId +import io.bkbn.kompendium.core.util.complexRequest +import io.bkbn.kompendium.core.util.customAuthConfig +import io.bkbn.kompendium.core.util.customFieldNameResponse +import io.bkbn.kompendium.core.util.dateTimeString +import io.bkbn.kompendium.core.util.defaultAuthConfig +import io.bkbn.kompendium.core.util.defaultField +import io.bkbn.kompendium.core.util.defaultParameter +import io.bkbn.kompendium.core.util.exampleParams +import io.bkbn.kompendium.core.util.genericException +import io.bkbn.kompendium.core.util.genericPolymorphicResponse +import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls +import io.bkbn.kompendium.core.util.gnarlyGenericResponse +import io.bkbn.kompendium.core.util.headerParameter +import io.bkbn.kompendium.core.util.ignoredFieldsResponse +import io.bkbn.kompendium.core.util.multipleAuthStrategies +import io.bkbn.kompendium.core.util.multipleExceptions +import io.bkbn.kompendium.core.util.nestedGenericCollection +import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection +import io.bkbn.kompendium.core.util.nestedGenericResponse +import io.bkbn.kompendium.core.util.nestedTypeName +import io.bkbn.kompendium.core.util.nonRequiredParam +import io.bkbn.kompendium.core.util.nonRequiredParams +import io.bkbn.kompendium.core.util.notarizedDelete +import io.bkbn.kompendium.core.util.notarizedGet +import io.bkbn.kompendium.core.util.notarizedHead +import io.bkbn.kompendium.core.util.notarizedOptions +import io.bkbn.kompendium.core.util.notarizedPatch +import io.bkbn.kompendium.core.util.notarizedPost +import io.bkbn.kompendium.core.util.notarizedPut +import io.bkbn.kompendium.core.util.nullableEnumField +import io.bkbn.kompendium.core.util.nullableField +import io.bkbn.kompendium.core.util.nullableNestedObject +import io.bkbn.kompendium.core.util.nullableReference +import io.bkbn.kompendium.core.util.overrideMediaTypes +import io.bkbn.kompendium.core.util.polymorphicCollectionResponse +import io.bkbn.kompendium.core.util.polymorphicException +import io.bkbn.kompendium.core.util.polymorphicMapResponse +import io.bkbn.kompendium.core.util.polymorphicResponse +import io.bkbn.kompendium.core.util.primitives +import io.bkbn.kompendium.core.util.reqRespExamples +import io.bkbn.kompendium.core.util.requiredParams +import io.bkbn.kompendium.core.util.returnsList +import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth +import io.bkbn.kompendium.core.util.samePathSameMethod +import io.bkbn.kompendium.core.util.simpleGenericResponse +import io.bkbn.kompendium.core.util.simpleRecursive +import io.bkbn.kompendium.core.util.singleException +import io.bkbn.kompendium.core.util.topLevelNullable +import io.bkbn.kompendium.core.util.unbackedFieldsResponse +import io.bkbn.kompendium.core.util.withOperationId +import io.bkbn.kompendium.core.util.nestedUnderRoot +import io.bkbn.kompendium.core.util.rootRoute +import io.bkbn.kompendium.core.util.simplePathParsing +import io.bkbn.kompendium.core.util.trailingSlash import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException import io.bkbn.kompendium.oas.component.Components @@ -75,8 +77,8 @@ import io.ktor.server.auth.UserIdPrincipal import io.ktor.server.auth.basic import io.ktor.server.auth.jwt.jwt import io.ktor.server.auth.oauth -import kotlin.reflect.typeOf import java.time.Instant +import kotlin.reflect.typeOf class KompendiumTest : DescribeSpec({ describe("Notarized Open API Metadata Tests") { @@ -251,12 +253,42 @@ class KompendiumTest : DescribeSpec({ it("Can handle top level nullable types") { openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() } } + it("Can handle multiple registrations for different methods with the same path and different auth") { + openApiTestAllSerializers( + "T0053__same_path_different_methods_and_auth.json", + applicationSetup = { + install(Authentication) { + basic("basic") { + realm = "Ktor Server" + validate { UserIdPrincipal("Placeholder") } + } + } + }, + specOverrides = { + this.copy( + components = Components( + securitySchemes = mutableMapOf( + "basic" to BasicAuth() + ) + ) + ) + } + ) { samePathDifferentMethodsAndAuth() } + } } describe("Error Handling") { it("Throws a clear exception when an unidentified type is encountered") { val exception = shouldThrow { openApiTestAllSerializers("") { dateTimeString() } } exception.message should startWith("An unknown type was encountered: class java.time.Instant") } + it("Throws an exception when same method for same path has been previously registered") { + val exception = shouldThrow { + openApiTestAllSerializers("") { + samePathSameMethod() + } + } + exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET") + } } describe("Constraints") { // TODO Assess strategies here diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt new file mode 100644 index 000000000..fac73e21f --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Authentication.kt @@ -0,0 +1,51 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.core.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.auth.authenticate +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.defaultAuthConfig() { + authenticate("basic") { + route(rootPath) { + basicGetGenerator() + } + } +} + +fun Routing.customAuthConfig() { + authenticate("auth-oauth-google") { + route(rootPath) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + security = mapOf( + "auth-oauth-google" to listOf("read:pets") + ) + } + } + } + } +} + +fun Routing.multipleAuthStrategies() { + authenticate("jwt", "api-key") { + route(rootPath) { + basicGetGenerator() + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/CustomSerializableReader.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/CustomSerializableReader.kt new file mode 100644 index 000000000..ddc8150e9 --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/CustomSerializableReader.kt @@ -0,0 +1,10 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.SerialNameObject +import io.bkbn.kompendium.core.fixtures.TransientObject +import io.bkbn.kompendium.core.fixtures.UnbackedObject +import io.ktor.server.routing.Routing + +fun Routing.ignoredFieldsResponse() = basicGetGenerator() +fun Routing.unbackedFieldsResponse() = basicGetGenerator() +fun Routing.customFieldNameResponse() = basicGetGenerator() diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Defaults.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Defaults.kt new file mode 100644 index 000000000..71aa4513e --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Defaults.kt @@ -0,0 +1,16 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.server.routing.Routing + +fun Routing.defaultParameter() = basicGetGenerator( + params = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING.withDefault("IDK") + ) + ) +) diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/ErrorHandling.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/ErrorHandling.kt new file mode 100644 index 000000000..2ad8d8003 --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/ErrorHandling.kt @@ -0,0 +1,16 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.util.TestModules.defaultPath +import io.ktor.server.auth.authenticate +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.samePathSameMethod() { + route(defaultPath) { + basicGetGenerator() + authenticate { + basicGetGenerator() + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt new file mode 100644 index 000000000..e98351faf --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Examples.kt @@ -0,0 +1,57 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.TestNested +import io.bkbn.kompendium.core.fixtures.TestRequest +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.metadata.PostInfo +import io.bkbn.kompendium.core.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.reqRespExamples() { + route(rootPath) { + install(NotarizedRoute()) { + post = PostInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + description(defaultRequestDescription) + requestType() + examples( + "Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) + ) + } + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + examples( + "Testerino" to TestResponse("Heya") + ) + } + } + } + } +} + +fun Routing.exampleParams() = basicGetGenerator( + params = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING, + examples = mapOf( + "foo" to Parameter.Example("testing") + ) + ) + ) +) diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Exceptions.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Exceptions.kt new file mode 100644 index 000000000..796a556db --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Exceptions.kt @@ -0,0 +1,105 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.ExceptionResponse +import io.bkbn.kompendium.core.fixtures.Flibbity +import io.bkbn.kompendium.core.fixtures.FlibbityGibbit +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.core.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.singleException() { + route(rootPath) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + canRespond { + description("Bad Things Happened") + responseCode(HttpStatusCode.BadRequest) + responseType() + } + } + } + } +} + +fun Routing.multipleExceptions() { + route(rootPath) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + canRespond { + description("Bad Things Happened") + responseCode(HttpStatusCode.BadRequest) + responseType() + } + canRespond { + description("Access Denied") + responseCode(HttpStatusCode.Forbidden) + responseType() + } + } + } + } +} + +fun Routing.polymorphicException() { + route(rootPath) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + canRespond { + description("Bad Things Happened") + responseCode(HttpStatusCode.InternalServerError) + responseType() + } + } + } + } +} + +fun Routing.genericException() { + route(rootPath) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + canRespond { + description("Bad Things Happened") + responseCode(HttpStatusCode.BadRequest) + responseType>() + } + } + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Miscellaneous.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Miscellaneous.kt new file mode 100644 index 000000000..17a44c74d --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Miscellaneous.kt @@ -0,0 +1,80 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.ColumnSchema +import io.bkbn.kompendium.core.fixtures.DateTimeString +import io.bkbn.kompendium.core.fixtures.ManyThings +import io.bkbn.kompendium.core.fixtures.Nested +import io.bkbn.kompendium.core.fixtures.NullableEnum +import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest +import io.bkbn.kompendium.core.fixtures.TestCreatedResponse +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.fixtures.TestSimpleRequest +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.core.metadata.PutInfo +import io.bkbn.kompendium.core.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultParams +import io.bkbn.kompendium.core.util.TestModules.defaultPath +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.auth.authenticate +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.withOperationId() = basicGetGenerator(operationId = "getThisDude") +fun Routing.nullableNestedObject() = basicGetGenerator() +fun Routing.nullableEnumField() = basicGetGenerator() +fun Routing.nullableReference() = basicGetGenerator() +fun Routing.dateTimeString() = basicGetGenerator() +fun Routing.headerParameter() = basicGetGenerator( + params = listOf( + Parameter( + name = "X-User-Email", + `in` = Parameter.Location.header, + schema = TypeDefinition.STRING, + required = true + ) + ) +) + +fun Routing.nestedTypeName() = basicGetGenerator() +fun Routing.topLevelNullable() = basicGetGenerator() +fun Routing.simpleRecursive() = basicGetGenerator() +fun Routing.samePathDifferentMethodsAndAuth() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + } + } + authenticate("basic") { + install(NotarizedRoute()) { + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + description(defaultRequestDescription) + requestType() + } + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.Created) + responseType() + } + } + } + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt new file mode 100644 index 000000000..5351e3ba9 --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt @@ -0,0 +1,301 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.ComplexRequest +import io.bkbn.kompendium.core.fixtures.TestCreatedResponse +import io.bkbn.kompendium.core.fixtures.TestRequest +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.fixtures.TestSimpleRequest +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.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultParams +import io.bkbn.kompendium.core.util.TestModules.defaultPath +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Routing +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.head +import io.ktor.server.routing.options +import io.ktor.server.routing.patch +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route + +fun Routing.notarizedGet() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + get = GetInfo.builder { + response { + responseCode(HttpStatusCode.OK) + responseType() + description(defaultResponseDescription) + } + summary(defaultPathSummary) + description(defaultPathDescription) + } + } + get { + call.respondText { "hey dude ‼️ congrats on the get request" } + } + } +} + +fun Routing.notarizedPost() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + post = PostInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + requestType() + description("A Test request") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(defaultResponseDescription) + } + } + } + post { + call.respondText { "hey dude ‼️ congrats on the post request" } + } + } +} + +fun Routing.notarizedPut() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + requestType() + description("A Test request") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(defaultResponseDescription) + } + } + } + put { + call.respondText { "hey dude ‼️ congrats on the post request" } + } + } +} + +fun Routing.notarizedDelete() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + delete = DeleteInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + responseCode(HttpStatusCode.NoContent) + responseType() + description(defaultResponseDescription) + } + } + } + } + delete { + call.respond(HttpStatusCode.NoContent) + } +} + +fun Routing.notarizedPatch() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + patch = PatchInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + description("A Test request") + requestType() + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(defaultResponseDescription) + } + } + } + patch { + call.respond(HttpStatusCode.Created) { TestCreatedResponse(123, "Nice!") } + } + } +} + +fun Routing.notarizedHead() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + head = HeadInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + + response { + description("great!") + responseCode(HttpStatusCode.Created) + responseType() + } + } + } + head { + call.respond(HttpStatusCode.OK) + } + } +} + +fun Routing.notarizedOptions() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + options = OptionsInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + responseCode(HttpStatusCode.OK) + responseType() + description("nice") + } + } + } + options { + call.respond(HttpStatusCode.NoContent) + } + } +} + +fun Routing.complexRequest() { + route(rootPath) { + install(NotarizedRoute()) { + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + requestType() + description("A Complex request") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(defaultResponseDescription) + } + } + } + patch { + call.respond(HttpStatusCode.Created, TestCreatedResponse(123, "nice!")) + } + } +} + +fun Routing.primitives() { + route(rootPath) { + install(NotarizedRoute()) { + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + requestType() + description("A Test Request") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(defaultResponseDescription) + } + } + } + } +} + +fun Routing.returnsList() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description("A Successful List-y Endeavor") + responseCode(HttpStatusCode.OK) + responseType>() + } + } + } + } +} + +fun Routing.nonRequiredParams() { + route("/optional") { + install(NotarizedRoute()) { + parameters = listOf( + Parameter( + name = "notRequired", + `in` = Parameter.Location.query, + schema = TypeDefinition.STRING, + required = false, + ), + Parameter( + name = "required", + `in` = Parameter.Location.query, + schema = TypeDefinition.STRING + ) + ) + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + responseType() + description("Empty") + responseCode(HttpStatusCode.NoContent) + } + } + } + } +} + +fun Routing.overrideMediaTypes() { + route("/media_types") { + install(NotarizedRoute()) { + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + mediaTypes("multipart/form-data", "application/json") + requestType() + description("A cool request") + } + response { + mediaTypes("application/xml") + responseType() + description("A good response") + responseCode(HttpStatusCode.Created) + } + } + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt new file mode 100644 index 000000000..2e07f1ff8 --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/PolymorphismAndGenerics.kt @@ -0,0 +1,22 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.Barzo +import io.bkbn.kompendium.core.fixtures.ComplexRequest +import io.bkbn.kompendium.core.fixtures.Flibbity +import io.bkbn.kompendium.core.fixtures.FlibbityGibbit +import io.bkbn.kompendium.core.fixtures.Foosy +import io.bkbn.kompendium.core.fixtures.Gibbity +import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics +import io.bkbn.kompendium.core.fixtures.Page +import io.ktor.server.routing.Routing + +fun Routing.polymorphicResponse() = basicGetGenerator() +fun Routing.polymorphicCollectionResponse() = basicGetGenerator>() +fun Routing.polymorphicMapResponse() = basicGetGenerator>() +fun Routing.simpleGenericResponse() = basicGetGenerator>() +fun Routing.gnarlyGenericResponse() = basicGetGenerator, String>>() +fun Routing.nestedGenericResponse() = basicGetGenerator>>() +fun Routing.genericPolymorphicResponse() = basicGetGenerator>() +fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator>() +fun Routing.nestedGenericCollection() = basicGetGenerator>() +fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator>() diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/RequiredFields.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/RequiredFields.kt new file mode 100644 index 000000000..6ae24cd3f --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/RequiredFields.kt @@ -0,0 +1,32 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.DefaultField +import io.bkbn.kompendium.core.fixtures.NullableField +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.server.routing.Routing + +fun Routing.requiredParams() = basicGetGenerator( + params = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ) +) + +fun Routing.nonRequiredParam() = basicGetGenerator( + params = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.query, + schema = TypeDefinition.STRING, + required = false + ) + ) +) + +fun Routing.defaultField() = basicGetGenerator() +fun Routing.nullableField() = basicGetGenerator() diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/RouteParsing.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/RouteParsing.kt new file mode 100644 index 000000000..eead3dc5b --- /dev/null +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/RouteParsing.kt @@ -0,0 +1,102 @@ +package io.bkbn.kompendium.core.util + +import io.bkbn.kompendium.core.fixtures.TestResponse +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.core.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultParams +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.routing.Routing +import io.ktor.server.routing.route + +fun Routing.simplePathParsing() { + route("/this") { + route("/is") { + route("/a") { + route("/complex") { + route("path") { + route("with/an/{id}") { + install(NotarizedRoute()) { + get = GetInfo.builder { + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ) + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + } + } + } + } + } + } + } + } +} + +fun Routing.rootRoute() { + route(rootPath) { + install(NotarizedRoute()) { + parameters = listOf(defaultParams.last()) + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + } + } + } +} + +fun Routing.nestedUnderRoot() { + route("/") { + route("/testerino") { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + } + } + } + } +} + +fun Routing.trailingSlash() { + route("/test") { + route("/") { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() + } + } + } + } + } +} diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt index 680cb8f72..77b6513b8 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt @@ -1,66 +1,29 @@ package io.bkbn.kompendium.core.util -import io.bkbn.kompendium.core.fixtures.Barzo -import io.bkbn.kompendium.core.fixtures.ColumnSchema -import io.bkbn.kompendium.core.fixtures.ComplexRequest -import io.bkbn.kompendium.core.fixtures.DateTimeString -import io.bkbn.kompendium.core.fixtures.DefaultField -import io.bkbn.kompendium.core.fixtures.ExceptionResponse -import io.bkbn.kompendium.core.fixtures.Flibbity -import io.bkbn.kompendium.core.fixtures.FlibbityGibbit -import io.bkbn.kompendium.core.fixtures.Foosy -import io.bkbn.kompendium.core.fixtures.Gibbity -import io.bkbn.kompendium.core.fixtures.ManyThings -import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics -import io.bkbn.kompendium.core.fixtures.Nested -import io.bkbn.kompendium.core.fixtures.NullableEnum -import io.bkbn.kompendium.core.fixtures.NullableField -import io.bkbn.kompendium.core.fixtures.Page -import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest -import io.bkbn.kompendium.core.fixtures.SerialNameObject -import io.bkbn.kompendium.core.fixtures.TestCreatedResponse -import io.bkbn.kompendium.core.fixtures.TestNested -import io.bkbn.kompendium.core.fixtures.TestRequest -import io.bkbn.kompendium.core.fixtures.TestResponse -import io.bkbn.kompendium.core.fixtures.TestSimpleRequest -import io.bkbn.kompendium.core.fixtures.TransientObject -import io.bkbn.kompendium.core.fixtures.UnbackedObject -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.plugin.NotarizedRoute +import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription +import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary +import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription +import io.bkbn.kompendium.core.util.TestModules.rootPath import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.oas.payload.Parameter import io.ktor.http.HttpStatusCode -import io.ktor.server.application.call import io.ktor.server.application.install -import io.ktor.server.auth.authenticate -import io.ktor.server.response.respond -import io.ktor.server.response.respondText import io.ktor.server.routing.Route import io.ktor.server.routing.Routing -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.head -import io.ktor.server.routing.options -import io.ktor.server.routing.patch -import io.ktor.server.routing.post -import io.ktor.server.routing.put import io.ktor.server.routing.route object TestModules { - private const val defaultPath = "/test/{a}" - private const val rootPath = "/" - private const val defaultResponseDescription = "A Successful Endeavor" - private const val defaultRequestDescription = "You gotta send it" - private const val defaultPathSummary = "Great Summary!" - private const val defaultPathDescription = "testing more" - private val defaultParams = listOf( + const val defaultPath = "/test/{a}" + const val rootPath = "/" + const val defaultResponseDescription = "A Successful Endeavor" + const val defaultRequestDescription = "You gotta send it" + const val defaultPathSummary = "Great Summary!" + const val defaultPathDescription = "testing more" + + val defaultParams = listOf( Parameter( name = "a", `in` = Parameter.Location.path, @@ -72,634 +35,31 @@ object TestModules { schema = TypeDefinition.INT ) ) +} - fun Routing.notarizedGet() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - get = GetInfo.builder { - response { - responseCode(HttpStatusCode.OK) - responseType() - description(defaultResponseDescription) - } - summary(defaultPathSummary) - description(defaultPathDescription) - } - } - get { - call.respondText { "hey dude ‼️ congrats on the get request" } - } - } +internal inline fun Routing.basicGetGenerator( + params: List = emptyList(), + operationId: String? = null +) { + route(rootPath) { + basicGetGenerator(params, operationId) } +} - fun Routing.notarizedPost() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - post = PostInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - requestType() - description("A Test request") - } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(defaultResponseDescription) - } - } - } - post { - call.respondText { "hey dude ‼️ congrats on the post request" } - } - } - } - - fun Routing.notarizedPut() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - put = PutInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - requestType() - description("A Test request") - } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(defaultResponseDescription) - } - } - } - put { - call.respondText { "hey dude ‼️ congrats on the post request" } - } - } - } - - fun Routing.notarizedDelete() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - delete = DeleteInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - responseCode(HttpStatusCode.NoContent) - responseType() - description(defaultResponseDescription) - } - } - } - } - delete { - call.respond(HttpStatusCode.NoContent) - } - } - - fun Routing.notarizedPatch() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - patch = PatchInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - description("A Test request") - requestType() - } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(defaultResponseDescription) - } - } - } - patch { - call.respond(HttpStatusCode.Created) { TestCreatedResponse(123, "Nice!") } - } - } - } - - fun Routing.notarizedHead() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - head = HeadInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - - response { - description("great!") - responseCode(HttpStatusCode.Created) - responseType() - } - } - } - head { - call.respond(HttpStatusCode.OK) - } - } - } - - fun Routing.notarizedOptions() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - options = OptionsInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - responseCode(HttpStatusCode.OK) - responseType() - description("nice") - } - } - } - options { - call.respond(HttpStatusCode.NoContent) - } - } - } - - fun Routing.complexRequest() { - route(rootPath) { - install(NotarizedRoute()) { - put = PutInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - requestType() - description("A Complex request") - } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(defaultResponseDescription) - } - } - } - patch { - call.respond(HttpStatusCode.Created, TestCreatedResponse(123, "nice!")) - } - } - } - - fun Routing.primitives() { - route(rootPath) { - install(NotarizedRoute()) { - put = PutInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - requestType() - description("A Test Request") - } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(defaultResponseDescription) - } - } - } - } - } - - fun Routing.returnsList() { - route(defaultPath) { - install(NotarizedRoute()) { - parameters = defaultParams - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description("A Successful List-y Endeavor") - responseCode(HttpStatusCode.OK) - responseType>() - } - } - } - } - } - - fun Routing.nonRequiredParams() { - route("/optional") { - install(NotarizedRoute()) { - parameters = listOf( - Parameter( - name = "notRequired", - `in` = Parameter.Location.query, - schema = TypeDefinition.STRING, - required = false, - ), - Parameter( - name = "required", - `in` = Parameter.Location.query, - schema = TypeDefinition.STRING - ) - ) - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - responseType() - description("Empty") - responseCode(HttpStatusCode.NoContent) - } - } - } - } - } - - fun Routing.overrideMediaTypes() { - route("/media_types") { - install(NotarizedRoute()) { - put = PutInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - mediaTypes("multipart/form-data", "application/json") - requestType() - description("A cool request") - } - response { - mediaTypes("application/xml") - responseType() - description("A good response") - responseCode(HttpStatusCode.Created) - } - } - } - } - } - - fun Routing.simplePathParsing() { - route("/this") { - route("/is") { - route("/a") { - route("/complex") { - route("path") { - route("with/an/{id}") { - install(NotarizedRoute()) { - get = GetInfo.builder { - parameters = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING - ) - ) - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - } - } - } - } - } - } - } - } - } - - fun Routing.rootRoute() { - route(rootPath) { - install(NotarizedRoute()) { - parameters = listOf(defaultParams.last()) - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - } - } - } - } - - fun Routing.nestedUnderRoot() { - route("/") { - route("/testerino") { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - } - } - } - } - } - - fun Routing.trailingSlash() { - route("/test") { - route("/") { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - } - } - } - } - } - - fun Routing.singleException() { - route(rootPath) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - canRespond { - description("Bad Things Happened") - responseCode(HttpStatusCode.BadRequest) - responseType() - } - } - } - } - } - - fun Routing.multipleExceptions() { - route(rootPath) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - canRespond { - description("Bad Things Happened") - responseCode(HttpStatusCode.BadRequest) - responseType() - } - canRespond { - description("Access Denied") - responseCode(HttpStatusCode.Forbidden) - responseType() - } - } - } - } - } - - fun Routing.polymorphicException() { - route(rootPath) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - canRespond { - description("Bad Things Happened") - responseCode(HttpStatusCode.InternalServerError) - responseType() - } - } - } - } - } - - fun Routing.genericException() { - route(rootPath) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - canRespond { - description("Bad Things Happened") - responseCode(HttpStatusCode.BadRequest) - responseType>() - } - } - } - } - } - - fun Routing.reqRespExamples() { - route(rootPath) { - install(NotarizedRoute()) { - post = PostInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - request { - description(defaultRequestDescription) - requestType() - examples( - "Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList()) - ) - } - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - examples( - "Testerino" to TestResponse("Heya") - ) - } - } - } - } - } - - fun Routing.exampleParams() = basicGetGenerator( - params = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING, - examples = mapOf( - "foo" to Parameter.Example("testing") - ) - ) - ) - ) - - fun Routing.defaultParameter() = basicGetGenerator( - params = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING.withDefault("IDK") - ) - ) - ) - - fun Routing.requiredParams() = basicGetGenerator( - params = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING - ) - ) - ) - - fun Routing.nonRequiredParam() = basicGetGenerator( - params = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.query, - schema = TypeDefinition.STRING, - required = false - ) - ) - ) - - fun Routing.defaultField() = basicGetGenerator() - - fun Routing.nullableField() = basicGetGenerator() - - fun Routing.polymorphicResponse() = basicGetGenerator() - - fun Routing.ignoredFieldsResponse() = basicGetGenerator() - - fun Routing.unbackedFieldsResponse() = basicGetGenerator() - - fun Routing.customFieldNameResponse() = basicGetGenerator() - - fun Routing.polymorphicCollectionResponse() = basicGetGenerator>() - - fun Routing.polymorphicMapResponse() = basicGetGenerator>() - - fun Routing.simpleGenericResponse() = basicGetGenerator>() - - fun Routing.gnarlyGenericResponse() = basicGetGenerator, String>>() - - fun Routing.nestedGenericResponse() = basicGetGenerator>>() - - fun Routing.genericPolymorphicResponse() = basicGetGenerator>() - - fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator>() - - fun Routing.nestedGenericCollection() = basicGetGenerator>() - - fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator>() - - fun Routing.withOperationId() = basicGetGenerator(operationId = "getThisDude") - - fun Routing.nullableNestedObject() = basicGetGenerator() - - fun Routing.nullableEnumField() = basicGetGenerator() - - fun Routing.nullableReference() = basicGetGenerator() - - fun Routing.dateTimeString() = basicGetGenerator() - - fun Routing.headerParameter() = basicGetGenerator( - params = listOf( - Parameter( - name = "X-User-Email", - `in` = Parameter.Location.header, - schema = TypeDefinition.STRING, - required = true - ) - ) - ) - - fun Routing.nestedTypeName() = basicGetGenerator() - - fun Routing.topLevelNullable() = basicGetGenerator() - - fun Routing.simpleRecursive() = basicGetGenerator() - - fun Routing.defaultAuthConfig() { - authenticate("basic") { - route(rootPath) { - basicGetGenerator() - } - } - } - - fun Routing.customAuthConfig() { - authenticate("auth-oauth-google") { - route(rootPath) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } - security = mapOf( - "auth-oauth-google" to listOf("read:pets") - ) - } - } - } - } - } - - fun Routing.multipleAuthStrategies() { - authenticate("jwt", "api-key") { - route(rootPath) { - basicGetGenerator() - } - } - } - - private inline fun Routing.basicGetGenerator( - params: List = emptyList(), - operationId: String? = null - ) { - route(rootPath) { - basicGetGenerator(params, operationId) - } - } - - private inline fun Route.basicGetGenerator( - params: List = emptyList(), - operationId: String? = null - ) { - install(NotarizedRoute()) { - get = GetInfo.builder { - summary(defaultPathSummary) - description(defaultPathDescription) - operationId?.let { operationId(it) } - parameters = params - response { - description(defaultResponseDescription) - responseCode(HttpStatusCode.OK) - responseType() - } +internal inline fun Route.basicGetGenerator( + params: List = emptyList(), + operationId: String? = null +) { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + operationId?.let { operationId(it) } + parameters = params + response { + description(defaultResponseDescription) + responseCode(HttpStatusCode.OK) + responseType() } } } diff --git a/core/src/test/resources/T0053__same_path_different_methods_and_auth.json b/core/src/test/resources/T0053__same_path_different_methods_and_auth.json new file mode 100644 index 000000000..03607a7f7 --- /dev/null +++ b/core/src/test/resources/T0053__same_path_different_methods_and_auth.json @@ -0,0 +1,164 @@ +{ + "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": { + "/test/{a}": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestResponse" + } + } + } + } + }, + "deprecated": false + }, + "put": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "requestBody": { + "description": "You gotta send it", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestSimpleRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestCreatedResponse" + } + } + } + } + }, + "deprecated": false, + "security": [ + { + "basic": [] + } + ] + }, + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true, + "deprecated": false + }, + { + "name": "aa", + "in": "query", + "schema": { + "type": "number", + "format": "int32" + }, + "required": true, + "deprecated": false + } + ] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "TestResponse": { + "type": "object", + "properties": { + "c": { + "type": "string" + } + }, + "required": [ + "c" + ] + }, + "TestCreatedResponse": { + "type": "object", + "properties": { + "c": { + "type": "string" + }, + "id": { + "type": "number", + "format": "int32" + } + }, + "required": [ + "c", + "id" + ] + }, + "TestSimpleRequest": { + "type": "object", + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "number", + "format": "int32" + } + }, + "required": [ + "a", + "b" + ] + } + }, + "securitySchemes": { + "basic": { + "type": "http", + "scheme": "basic" + } + } + }, + "security": [], + "tags": [] +} diff --git a/docs/plugins/notarized_route.md b/docs/plugins/notarized_route.md index 77af1959d..2eab404dd 100644 --- a/docs/plugins/notarized_route.md +++ b/docs/plugins/notarized_route.md @@ -183,3 +183,24 @@ get = GetInfo.builder { } } ``` + +## Partial Authentication + +One might want to have a public GET endpoint but a protected PUT endpoint. This can be achieved by registering two +separate notarized routes. Note that you will get an error if you try to register the same method twice, as each path +can only have one registration per method. Example: + +```kotlin +route("/user/{id}") { + get = GetInfo.builder { + // ... + } + // ... + authenticate { + put = PutInfo.builder { + // ... + } + // ... + } +} +``` diff --git a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt index 2c044b028..c0bb19998 100644 --- a/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt +++ b/locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocations.kt @@ -51,17 +51,17 @@ object NotarizedLocations { val spec = application.attributes[KompendiumAttributes.openApiSpec] val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] pluginConfig.locations.forEach { (k, v) -> - val path = Path() - path.parameters = v.parameters - v.get?.addToSpec(path, spec, v, serializableReader) - v.delete?.addToSpec(path, spec, v, serializableReader) - v.head?.addToSpec(path, spec, v, serializableReader) - v.options?.addToSpec(path, spec, v, serializableReader) - v.post?.addToSpec(path, spec, v, serializableReader) - v.put?.addToSpec(path, spec, v, serializableReader) - v.patch?.addToSpec(path, spec, v, serializableReader) - val location = k.getLocationFromClass() + val path = spec.paths[location] ?: Path() + path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters + v.get?.addToSpec(path, spec, v, serializableReader, location) + v.delete?.addToSpec(path, spec, v, serializableReader, location) + v.head?.addToSpec(path, spec, v, serializableReader, location) + v.options?.addToSpec(path, spec, v, serializableReader, location) + v.post?.addToSpec(path, spec, v, serializableReader, location) + v.put?.addToSpec(path, spec, v, serializableReader, location) + v.patch?.addToSpec(path, spec, v, serializableReader, location) + spec.paths[location] = path } } diff --git a/resources/src/main/kotlin/io/bkbn/kompendium/resources/NotarizedResources.kt b/resources/src/main/kotlin/io/bkbn/kompendium/resources/NotarizedResources.kt index 8b60bf0ca..47eadf8c5 100644 --- a/resources/src/main/kotlin/io/bkbn/kompendium/resources/NotarizedResources.kt +++ b/resources/src/main/kotlin/io/bkbn/kompendium/resources/NotarizedResources.kt @@ -45,17 +45,17 @@ object NotarizedResources { val spec = application.attributes[KompendiumAttributes.openApiSpec] val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] pluginConfig.resources.forEach { (k, v) -> - val path = Path() - path.parameters = v.parameters - v.get?.addToSpec(path, spec, v, serializableReader) - v.delete?.addToSpec(path, spec, v, serializableReader) - v.head?.addToSpec(path, spec, v, serializableReader) - v.options?.addToSpec(path, spec, v, serializableReader) - v.post?.addToSpec(path, spec, v, serializableReader) - v.put?.addToSpec(path, spec, v, serializableReader) - v.patch?.addToSpec(path, spec, v, serializableReader) - val resource = k.getResourcesFromClass() + val path = spec.paths[resource] ?: Path() + path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters + v.get?.addToSpec(path, spec, v, serializableReader, resource) + v.delete?.addToSpec(path, spec, v, serializableReader, resource) + v.head?.addToSpec(path, spec, v, serializableReader, resource) + v.options?.addToSpec(path, spec, v, serializableReader, resource) + v.post?.addToSpec(path, spec, v, serializableReader, resource) + v.put?.addToSpec(path, spec, v, serializableReader, resource) + v.patch?.addToSpec(path, spec, v, serializableReader, resource) + spec.paths[resource] = path } }