fix: do not include rootPath in serialized route path
This commit is contained in:
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Fix integration with application `rootPath` which is removed when `NotarizedRoute` resolve path.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
### Remove
|
### Remove
|
||||||
|
@ -18,6 +18,7 @@ import io.ktor.server.application.Hook
|
|||||||
import io.ktor.server.application.PluginBuilder
|
import io.ktor.server.application.PluginBuilder
|
||||||
import io.ktor.server.application.createRouteScopedPlugin
|
import io.ktor.server.application.createRouteScopedPlugin
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.application
|
||||||
|
|
||||||
object NotarizedRoute {
|
object NotarizedRoute {
|
||||||
class Config : SpecConfig {
|
class Config : SpecConfig {
|
||||||
@ -43,7 +44,6 @@ object NotarizedRoute {
|
|||||||
name = "NotarizedRoute",
|
name = "NotarizedRoute",
|
||||||
createConfiguration = ::Config
|
createConfiguration = ::Config
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// This is required in order to introspect the route path and authentication
|
// This is required in order to introspect the route path and authentication
|
||||||
on(InstallHook) {
|
on(InstallHook) {
|
||||||
val route = it as? Route ?: return@on
|
val route = it as? Route ?: return@on
|
||||||
@ -76,7 +76,17 @@ object NotarizedRoute {
|
|||||||
spec.paths[fullPath] = path
|
spec.paths[fullPath] = path
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
|
fun Route.calculateRoutePath() = toString()
|
||||||
|
.let {
|
||||||
|
application.environment.rootPath.takeIf { root -> root.isNotEmpty() }
|
||||||
|
?.let { root ->
|
||||||
|
val sanitizedRoute = if (root.startsWith("/")) root else "/$root"
|
||||||
|
it.replace(sanitizedRoute, "")
|
||||||
|
}
|
||||||
|
?: it
|
||||||
|
}
|
||||||
|
.replace(Regex("/\\(.+\\)"), "")
|
||||||
|
|
||||||
fun Route.collectAuthMethods() = toString()
|
fun Route.collectAuthMethods() = toString()
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter { it.contains(Regex("\\(authenticate .*\\)")) }
|
.filter { it.contains(Regex("\\(authenticate .*\\)")) }
|
||||||
|
@ -22,6 +22,7 @@ import io.bkbn.kompendium.core.util.nestedGenericCollection
|
|||||||
import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
|
import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
|
||||||
import io.bkbn.kompendium.core.util.nestedGenericResponse
|
import io.bkbn.kompendium.core.util.nestedGenericResponse
|
||||||
import io.bkbn.kompendium.core.util.nestedTypeName
|
import io.bkbn.kompendium.core.util.nestedTypeName
|
||||||
|
import io.bkbn.kompendium.core.util.nestedUnderRoot
|
||||||
import io.bkbn.kompendium.core.util.nonRequiredParam
|
import io.bkbn.kompendium.core.util.nonRequiredParam
|
||||||
import io.bkbn.kompendium.core.util.nonRequiredParams
|
import io.bkbn.kompendium.core.util.nonRequiredParams
|
||||||
import io.bkbn.kompendium.core.util.notarizedDelete
|
import io.bkbn.kompendium.core.util.notarizedDelete
|
||||||
@ -44,18 +45,17 @@ import io.bkbn.kompendium.core.util.primitives
|
|||||||
import io.bkbn.kompendium.core.util.reqRespExamples
|
import io.bkbn.kompendium.core.util.reqRespExamples
|
||||||
import io.bkbn.kompendium.core.util.requiredParams
|
import io.bkbn.kompendium.core.util.requiredParams
|
||||||
import io.bkbn.kompendium.core.util.returnsList
|
import io.bkbn.kompendium.core.util.returnsList
|
||||||
|
import io.bkbn.kompendium.core.util.rootRoute
|
||||||
import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
|
import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
|
||||||
import io.bkbn.kompendium.core.util.samePathSameMethod
|
import io.bkbn.kompendium.core.util.samePathSameMethod
|
||||||
import io.bkbn.kompendium.core.util.simpleGenericResponse
|
import io.bkbn.kompendium.core.util.simpleGenericResponse
|
||||||
|
import io.bkbn.kompendium.core.util.simplePathParsing
|
||||||
import io.bkbn.kompendium.core.util.simpleRecursive
|
import io.bkbn.kompendium.core.util.simpleRecursive
|
||||||
import io.bkbn.kompendium.core.util.singleException
|
import io.bkbn.kompendium.core.util.singleException
|
||||||
import io.bkbn.kompendium.core.util.topLevelNullable
|
import io.bkbn.kompendium.core.util.topLevelNullable
|
||||||
|
import io.bkbn.kompendium.core.util.trailingSlash
|
||||||
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
|
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
|
||||||
import io.bkbn.kompendium.core.util.withOperationId
|
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.definition.TypeDefinition
|
||||||
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
|
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
|
||||||
import io.bkbn.kompendium.oas.component.Components
|
import io.bkbn.kompendium.oas.component.Components
|
||||||
@ -77,6 +77,7 @@ import io.ktor.server.auth.UserIdPrincipal
|
|||||||
import io.ktor.server.auth.basic
|
import io.ktor.server.auth.basic
|
||||||
import io.ktor.server.auth.jwt.jwt
|
import io.ktor.server.auth.jwt.jwt
|
||||||
import io.ktor.server.auth.oauth
|
import io.ktor.server.auth.oauth
|
||||||
|
import java.net.URI
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
@ -275,6 +276,19 @@ class KompendiumTest : DescribeSpec({
|
|||||||
}
|
}
|
||||||
) { samePathDifferentMethodsAndAuth() }
|
) { samePathDifferentMethodsAndAuth() }
|
||||||
}
|
}
|
||||||
|
it("Can generate paths without application root-path") {
|
||||||
|
openApiTestAllSerializers(
|
||||||
|
"T0054__app_with_rootpath.json",
|
||||||
|
applicationEnvironmentBuilder = {
|
||||||
|
rootPath = "/example"
|
||||||
|
},
|
||||||
|
specOverrides = {
|
||||||
|
copy(
|
||||||
|
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { notarizedGet() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
describe("Error Handling") {
|
describe("Error Handling") {
|
||||||
it("Throws a clear exception when an unidentified type is encountered") {
|
it("Throws a clear exception when an unidentified type is encountered") {
|
||||||
|
92
core/src/test/resources/T0054__app_with_rootpath.json
Normal file
92
core/src/test/resources/T0054__app_with_rootpath.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"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/example",
|
||||||
|
"description": "Production instance of my API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://staging.myawesomeapi.com/example",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -21,13 +21,14 @@ import io.ktor.serialization.gson.gson
|
|||||||
import io.ktor.serialization.jackson.jackson
|
import io.ktor.serialization.jackson.jackson
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
import io.ktor.server.testing.ApplicationTestBuilder
|
import io.ktor.server.testing.ApplicationTestBuilder
|
||||||
import io.ktor.server.testing.testApplication
|
import io.ktor.server.testing.testApplication
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
|
||||||
object TestHelpers {
|
object TestHelpers {
|
||||||
private const val OPEN_API_ENDPOINT = "/openapi.json"
|
private const val OPEN_API_ENDPOINT = "/openapi.json"
|
||||||
@ -43,8 +44,8 @@ object TestHelpers {
|
|||||||
* exists as expected, and that the content matches the expected blob found in the specified file
|
* exists as expected, and that the content matches the expected blob found in the specified file
|
||||||
* @param snapshotName The snapshot file to retrieve from the resources folder
|
* @param snapshotName The snapshot file to retrieve from the resources folder
|
||||||
*/
|
*/
|
||||||
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(snapshotName: String) {
|
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(rootPath: String, snapshotName: String) {
|
||||||
val response = client.get(OPEN_API_ENDPOINT)
|
val response = client.get("$rootPath$OPEN_API_ENDPOINT")
|
||||||
response shouldHaveStatus HttpStatusCode.OK
|
response shouldHaveStatus HttpStatusCode.OK
|
||||||
response.bodyAsText() shouldNot beBlank()
|
response.bodyAsText() shouldNot beBlank()
|
||||||
response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName)
|
response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName)
|
||||||
@ -61,11 +62,36 @@ object TestHelpers {
|
|||||||
customTypes: Map<KType, JsonSchema> = emptyMap(),
|
customTypes: Map<KType, JsonSchema> = emptyMap(),
|
||||||
applicationSetup: Application.() -> Unit = { },
|
applicationSetup: Application.() -> Unit = { },
|
||||||
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
|
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
|
||||||
|
applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||||
routeUnderTest: Routing.() -> Unit
|
routeUnderTest: Routing.() -> Unit
|
||||||
) {
|
) {
|
||||||
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, specOverrides, customTypes)
|
openApiTest(
|
||||||
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, specOverrides, customTypes)
|
snapshotName,
|
||||||
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, specOverrides, customTypes)
|
SupportedSerializer.KOTLINX,
|
||||||
|
routeUnderTest,
|
||||||
|
applicationSetup,
|
||||||
|
specOverrides,
|
||||||
|
customTypes,
|
||||||
|
applicationEnvironmentBuilder
|
||||||
|
)
|
||||||
|
openApiTest(
|
||||||
|
snapshotName,
|
||||||
|
SupportedSerializer.JACKSON,
|
||||||
|
routeUnderTest,
|
||||||
|
applicationSetup,
|
||||||
|
specOverrides,
|
||||||
|
customTypes,
|
||||||
|
applicationEnvironmentBuilder
|
||||||
|
)
|
||||||
|
openApiTest(
|
||||||
|
snapshotName,
|
||||||
|
SupportedSerializer.GSON,
|
||||||
|
routeUnderTest,
|
||||||
|
applicationSetup,
|
||||||
|
specOverrides,
|
||||||
|
customTypes,
|
||||||
|
applicationEnvironmentBuilder
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openApiTest(
|
private fun openApiTest(
|
||||||
@ -74,8 +100,10 @@ object TestHelpers {
|
|||||||
routeUnderTest: Routing.() -> Unit,
|
routeUnderTest: Routing.() -> Unit,
|
||||||
applicationSetup: Application.() -> Unit,
|
applicationSetup: Application.() -> Unit,
|
||||||
specOverrides: OpenApiSpec.() -> OpenApiSpec,
|
specOverrides: OpenApiSpec.() -> OpenApiSpec,
|
||||||
typeOverrides: Map<KType, JsonSchema> = emptyMap()
|
typeOverrides: Map<KType, JsonSchema> = emptyMap(),
|
||||||
|
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {}
|
||||||
) = testApplication {
|
) = testApplication {
|
||||||
|
environment(applicationBuilder)
|
||||||
install(NotarizedApplication()) {
|
install(NotarizedApplication()) {
|
||||||
customTypes = typeOverrides
|
customTypes = typeOverrides
|
||||||
spec = defaultSpec().specOverrides()
|
spec = defaultSpec().specOverrides()
|
||||||
@ -105,6 +133,7 @@ object TestHelpers {
|
|||||||
redoc()
|
redoc()
|
||||||
routeUnderTest()
|
routeUnderTest()
|
||||||
}
|
}
|
||||||
compareOpenAPISpec(snapshotName)
|
val root = ApplicationEngineEnvironmentBuilder().apply(applicationBuilder).rootPath
|
||||||
|
compareOpenAPISpec(root, snapshotName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user