diff --git a/client/src/commonMain/kotlin/Client.kt b/client/src/commonMain/kotlin/Client.kt index 06ed990..cb61dc5 100644 --- a/client/src/commonMain/kotlin/Client.kt +++ b/client/src/commonMain/kotlin/Client.kt @@ -61,7 +61,9 @@ suspend inline fun { val result = request { method = endpoint.data.method - url(endpoint.path.toString()) + url { + pathSegments = endpoint.path.segments.map { it.text } + } for ((name, values) in endpoint.data.buildParameters(HashMap()).apply(parameters).data) for (value in values) diff --git a/server/src/commonTest/kotlin/BlankPathRegresssionTest.kt b/server/src/commonTest/kotlin/BlankPathRegresssionTest.kt new file mode 100644 index 0000000..771b98b --- /dev/null +++ b/server/src/commonTest/kotlin/BlankPathRegresssionTest.kt @@ -0,0 +1,52 @@ +package opensavvy.spine.typed.server + +import io.ktor.serialization.kotlinx.json.* +import opensavvy.prepared.compat.ktor.preparedClient +import opensavvy.prepared.compat.ktor.preparedServer +import opensavvy.prepared.suite.SuiteDsl +import opensavvy.spine.api.* +import opensavvy.spine.client.bodyOrThrow +import opensavvy.spine.client.request +import opensavvy.spine.server.respond +import opensavvy.spine.server.route +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +private object MinimalSpineApi : RootResource("api") { + object Profiles : StaticResource("profiles", MinimalSpineApi) { + object Username : DynamicResource("username", Profiles) { + val get by get() + .response() + } + } +} + +private val blankPathServer by preparedServer { + install(ServerContentNegotiation) { json() } + routing { + route(MinimalSpineApi.Profiles.Username.get) { + val id = idOf(MinimalSpineApi.Profiles.Username) + respond(id) + } + } +} + +private val blankPathClient by blankPathServer.preparedClient { + install(ClientContentNegotiation) { json() } +} + +fun SuiteDsl.blankPathRegressionTest() = suite("Blank path") { + test("URL-encoded spaces should be accepted as path segments") { + val result = blankPathClient().request( + MinimalSpineApi / MinimalSpineApi.Profiles / MinimalSpineApi.Profiles.Username("%20") / MinimalSpineApi.Profiles.Username.get + ).bodyOrThrow() + check(result == "%20") + } + + test("Literal whitespace should be accepted as path segments") { + val result = blankPathClient().request( + MinimalSpineApi / MinimalSpineApi.Profiles / MinimalSpineApi.Profiles.Username(" ") / MinimalSpineApi.Profiles.Username.get + ).bodyOrThrow() + check(result == " ") + } +} diff --git a/server/src/commonTest/kotlin/ServerTests.kt b/server/src/commonTest/kotlin/ServerTests.kt index c00d900..b075642 100644 --- a/server/src/commonTest/kotlin/ServerTests.kt +++ b/server/src/commonTest/kotlin/ServerTests.kt @@ -4,4 +4,5 @@ import opensavvy.prepared.runner.testballoon.preparedSuite val ServerTests by preparedSuite { routeTest() + blankPathRegressionTest() }