diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6414fef..e89ad22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ arrow = "2.2.3" # https://github.com/arrow-kt/arrow/releases kotlinx-coroutines = "1.11.0" # https://github.com/Kotlin/kotlinx.coroutines/releases +slf4j = "2.0.17" # https://central.sonatype.com/artifact/org.slf4j/slf4j-simple + [plugins] [libraries] @@ -24,4 +26,6 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } + [bundles] diff --git a/server/build.gradle.kts b/server/build.gradle.kts index eb7b9df..a46fba9 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -32,7 +32,9 @@ kotlin { implementation(libsCommon.kotlin.test) implementation(libs.ktor.server.contentNegotiation) implementation(libs.ktor.client.contentNegotiation) + implementation(libs.ktor.client.logging) implementation(libs.ktor.kotlinxJson) + implementation(libs.slf4j.simple) implementation(projects.client) implementation(projects.clientArrow) } diff --git a/server/src/commonTest/resources/simplelogger.properties b/server/src/commonTest/resources/simplelogger.properties new file mode 100644 index 0000000..e0f0d79 --- /dev/null +++ b/server/src/commonTest/resources/simplelogger.properties @@ -0,0 +1 @@ +org.slf4j.simpleLogger.defaultLogLevel=trace -- 2.51.2 From 8ad1df1c69fdaff069c54cdc9c2b8c91686c977a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 1 Jul 2026 09:30:11 +0200 Subject: [PATCH 2/2] fix(client): Fix whitespace being trimmed in request URLs Closes #31 --- client/src/commonMain/kotlin/Client.kt | 4 +- .../kotlin/BlankPathRegresssionTest.kt | 52 +++++++++++++++++++ server/src/commonTest/kotlin/ServerTests.kt | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 server/src/commonTest/kotlin/BlankPathRegresssionTest.kt 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() }