Skip to content

Commit 4ae9fe2

Browse files
authored
fix(#10): versioning is not properly handled for multiple versions (#11)
1 parent 0816e21 commit 4ae9fe2

File tree

8 files changed

+155
-11
lines changed

8 files changed

+155
-11
lines changed

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ kotlinx-coroutines = "1.6.4"
44
kotlinx-serialization = "1.4.1"
55
sqldelight = "2.0.0-alpha05"
66
rsocket = "0.15.4"
7+
ktor = "2.3.0"
8+
mockk = "1.13.5"
79

810
[libraries]
911
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
@@ -15,9 +17,13 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl
1517
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
1618
publish-library = { module = "publish-library:publish-library", version.require = "SNAPSHOT" }
1719
vanniktech-maven-publish = { module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version.require = "0.25.3" }
20+
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
21+
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
22+
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
1823

1924
[plugins]
2025
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
2126
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
2227
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2328
multiplatform-module-convention = { id = "multiplatform-module-convention", version.require = "SNAPSHOT" }
29+
example-module-convention = { id = "example-module-convention", version.require = "SNAPSHOT" }

router-versioning/core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ plugins {
55
dependencies {
66
commonMainImplementation(libs.rsocket.server)
77
commonMainImplementation(projects.routerCore)
8+
9+
jvmTestImplementation(libs.kotlin.test)
10+
jvmTestImplementation(projects.routerCore.test)
811
}
912

1013
mavenPublishing {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.y9vad9.rsocket.router.versioning
2+
3+
import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4+
import com.y9vad9.rsocket.router.builders.RouterBuilder
5+
import com.y9vad9.rsocket.router.versioning.preprocessor.VersionPreprocessor
6+
import io.ktor.utils.io.core.*
7+
import io.rsocket.kotlin.payload.Payload
8+
import kotlin.coroutines.CoroutineContext
9+
10+
@OptIn(ExperimentalInterceptorsApi::class)
11+
public fun RouterBuilder.versioning(
12+
block: (metadata: ByteReadPacket?, coroutineContext: CoroutineContext) -> Version,
13+
) {
14+
preprocessors {
15+
forCoroutineContext(object : VersionPreprocessor() {
16+
override fun version(payload: Payload, coroutineContext: CoroutineContext): Version {
17+
return block(payload.metadata, coroutineContext)
18+
}
19+
})
20+
}
21+
}

router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/Version.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import com.y9vad9.rsocket.router.versioning.preprocessor.VersionPreprocessor
55
import kotlin.coroutines.coroutineContext
66

77
/**
8-
* Represents a version number.
8+
* Represents a version with major, minor, and patch components.
99
*
10-
* @param double The numerical value of the version.
11-
* @throws IllegalArgumentException if the version is negative.
10+
* @property major The major version component.
11+
* @property minor The minor version component.
12+
* @property patch The patch version component.
13+
* @constructor Creates a Version instance with the specified major, minor, and patch components.
14+
* @throws IllegalArgumentException if major, minor, or patch is negative.
1215
*/
1316
public data class Version(public val major: Int, public val minor: Int, public val patch: Int = 0) : Comparable<Version> {
1417
init {
@@ -59,9 +62,13 @@ public data class Version(public val major: Int, public val minor: Int, public v
5962
*/
6063
public infix fun Version.until(another: Version): ClosedRange<Version> {
6164
return when {
62-
another.patch > 0 -> this .. another.copy(patch = patch - 1)
63-
another.minor > 0 -> this .. another.copy(minor = minor - 1)
64-
another.major > 0 -> this .. another.copy(major = major - 1)
65+
another.patch > 0 -> this .. another.copy(patch = another.patch - 1)
66+
another.minor > 0 -> this .. another.copy(minor = another.minor - 1, patch = Int.MAX_VALUE)
67+
another.major > 0 -> this .. another.copy(
68+
major = another.major - 1,
69+
minor = Int.MAX_VALUE,
70+
patch = Int.MAX_VALUE,
71+
)
6572
else -> error("Unable to create `until` range – version cannot be negative.")
6673
}
6774
}

router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/VersionedRequest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ internal sealed class VersionedRequest<T, R> {
5353

5454
return variants.firstOrNull { (requirement, _) ->
5555
requirement.satisfies(version)
56-
}?.second?.invoke(input) ?: throw RSocketError.Rejected("Request is not available for your API version.")
56+
}?.also { println(it.first) }?.second?.invoke(input) ?: throw RSocketError.Rejected("Request is not available for your API version.")
5757
}
5858
}
5959
}
6060

61+
6162
public data class PayloadStream(
6263
val initPayload: io.rsocket.kotlin.payload.Payload,
6364
val payloads: Flow<io.rsocket.kotlin.payload.Payload>,

router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/builders/VersioningBuilder.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class VersioningBuilder<T, R> internal constructor() {
1919
* @param version The new version to be applied.
2020
* @param block The coroutine block that will be executed for the given version.
2121
*/
22-
public fun version(version: Version, block: suspend (T) -> R): Unit {
22+
public fun version(version: Version, block: suspend (T) -> R) {
2323
versionedRequest = when (val versionedRequest = versionedRequest) {
2424
null -> {
2525
VersionedRequest.SingleConditional(
@@ -41,12 +41,14 @@ public class VersioningBuilder<T, R> internal constructor() {
4141

4242
is VersionedRequest.MultipleConditional<T, R> -> {
4343
versionedRequest.copy(
44-
variants = versionedRequest.variants.mapIndexed { index, (requirements, function) ->
44+
variants = (versionedRequest.variants.mapIndexed { index, (requirements, function) ->
4545
if (index == versionedRequest.variants.lastIndex)
4646
requirements.copy(
4747
lastAcceptableVersion = (requirements.firstAcceptableVersion until version).endInclusive,
4848
) to function
4949
else requirements to function
50+
} + (VersionRequirements(version, Version.INDEFINITE) to block)).sortedBy {
51+
it.first.firstAcceptableVersion
5052
}
5153
)
5254
}

router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/preprocessor/VersionPreprocessor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ public abstract class VersionPreprocessor : Preprocessor.CoroutineContext {
2121
* @param payload The payload containing the version information.
2222
* @return The version extracted from the payload.
2323
*/
24-
public abstract fun version(payload: Payload): Version
24+
public abstract fun version(payload: Payload, coroutineContext: CoroutineContext): Version
2525

2626
final override fun intercept(coroutineContext: CoroutineContext, input: Payload): CoroutineContext {
27-
return coroutineContext + VersionElement(version(input))
27+
return coroutineContext + VersionElement(version(input, coroutineContext))
2828
}
2929
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.y9vad9.rsocket.router.versioning.test
2+
3+
import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4+
import com.y9vad9.rsocket.router.router
5+
import com.y9vad9.rsocket.router.test.requestResponseOrAssert
6+
import com.y9vad9.rsocket.router.test.routeAtOrAssert
7+
import com.y9vad9.rsocket.router.versioning.Version
8+
import com.y9vad9.rsocket.router.versioning.builders.version
9+
import com.y9vad9.rsocket.router.versioning.preprocessor.VersionPreprocessor
10+
import com.y9vad9.rsocket.router.versioning.requestResponseV
11+
import com.y9vad9.rsocket.router.versioning.versioning
12+
import io.ktor.utils.io.core.*
13+
import io.rsocket.kotlin.RSocketError
14+
import io.rsocket.kotlin.payload.Payload
15+
import kotlinx.coroutines.runBlocking
16+
import kotlinx.coroutines.withContext
17+
import kotlin.test.Test
18+
import kotlin.test.assertEquals
19+
import kotlin.test.assertFailsWith
20+
21+
@OptIn(ExperimentalInterceptorsApi::class)
22+
class DeclarableRoutingBuilderExtTest {
23+
private val thirdRequestResponse = Payload(ByteReadPacket("test".toByteArray()))
24+
25+
private val router = router {
26+
routeProvider { error("Stub!") }
27+
versioning { _, _ -> error("Stub!") }
28+
29+
routing {
30+
route("test") {
31+
requestResponseV {
32+
version(2) { _ ->
33+
Payload.Empty
34+
}
35+
36+
version(3) { _ ->
37+
return@version thirdRequestResponse
38+
}
39+
}
40+
}
41+
}
42+
}
43+
44+
// это ахуенно
45+
@Test
46+
fun `test invalid version should fail`(): Unit = runBlocking {
47+
// GIVEN
48+
val context = VersionPreprocessor.VersionElement(Version(1, 0))
49+
50+
// THEN
51+
assertFailsWith<RSocketError.Rejected> {
52+
withContext(context) {
53+
router.routeAtOrAssert("test")
54+
.requestResponse(payload = Payload.Empty)
55+
}
56+
}
57+
}
58+
59+
@Test
60+
fun `test 2 version should pass within range`(): Unit = runBlocking {
61+
// GIVEN
62+
val contexts = listOf(
63+
VersionPreprocessor.VersionElement(Version(2, 0)),
64+
VersionPreprocessor.VersionElement(Version(2, 1)),
65+
VersionPreprocessor.VersionElement(Version(2, 9)),
66+
)
67+
68+
// WHEN
69+
repeat(contexts.size) { time ->
70+
withContext(contexts[time]) {
71+
val result = router.routeAtOrAssert("test")
72+
.requestResponseOrAssert(payload = Payload.Empty)
73+
74+
// THEN
75+
assertEquals(
76+
expected = Payload.Empty,
77+
actual = result,
78+
)
79+
}
80+
}
81+
}
82+
83+
@Test
84+
fun `test 3 version should be correct`(): Unit = runBlocking {
85+
// GIVEN
86+
val contexts = listOf(
87+
VersionPreprocessor.VersionElement(Version(3, 0)),
88+
VersionPreprocessor.VersionElement(Version(3, 9)),
89+
VersionPreprocessor.VersionElement(Version(4, 0)),
90+
)
91+
92+
repeat(contexts.size) { time ->
93+
withContext(contexts[time]) {
94+
val result = router.routeAtOrAssert("test")
95+
.requestResponseOrAssert(payload = Payload.Empty)
96+
97+
assertEquals(
98+
expected = thirdRequestResponse,
99+
actual = result,
100+
)
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)