├── .github
└── workflows
│ ├── publish-release.yml
│ └── run-tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── build-conventions
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── multiplatform-module-convention.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── router-core
├── build.gradle.kts
├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com.y9vad9.rsocket.router
│ │ ├── Route.kt
│ │ ├── Router.kt
│ │ ├── annotations
│ │ ├── ExperimentalInterceptorsApi.kt
│ │ ├── ExperimentalRouterApi.kt
│ │ ├── InternalRouterApi.kt
│ │ └── RouterDsl.kt
│ │ ├── builders
│ │ ├── DeclarableRoutingBuilder.kt
│ │ ├── RouterBuilder.kt
│ │ ├── RoutingBuilder.kt
│ │ └── impl
│ │ │ ├── DeclarableRoutingBuilderScopeImpl.kt
│ │ │ └── RoutingBuilderScopeImpl.kt
│ │ └── interceptors
│ │ ├── Interceptor.kt
│ │ └── builder
│ │ ├── PreprocessorsBuilder.kt
│ │ └── RouteInterceptorsBuilder.kt
└── test
│ ├── build.gradle.kts
│ └── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── test
│ │ ├── RouterExt.kt
│ │ └── Validators.kt
│ └── jvmTest
│ └── kotlin
│ └── com
│ └── y9vad9
│ └── rsocket
│ └── router
│ └── test
│ └── RouterTest.kt
├── router-serialization
├── README.md
├── cbor
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── serialization
│ │ └── json
│ │ └── CborContentSerializer.kt
├── core
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── serialization
│ │ ├── ContentSerializer.kt
│ │ ├── DeclarableRoutingBuilderExt.kt
│ │ ├── annotations
│ │ ├── ExperimentalRouterSerializationApi.kt
│ │ └── InternalRouterSerializationApi.kt
│ │ ├── context
│ │ └── SerializationContext.kt
│ │ └── preprocessor
│ │ └── SerializationProvider.kt
├── json
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── serialization
│ │ └── json
│ │ └── JsonContentSerializer.kt
├── protobuf
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── serialization
│ │ └── json
│ │ └── ProtobufContentSerializer.kt
└── test
│ ├── build.gradle.kts
│ └── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── serialization
│ │ └── test
│ │ └── RouterExt.kt
│ └── jvmTest
│ └── kotlin
│ └── com
│ └── y9vad9
│ └── rsocket
│ └── router
│ └── serialization
│ └── SerializableRouterTest.kt
├── router-versioning
├── README.md
├── core
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── y9vad9
│ │ │ └── rsocket
│ │ │ └── router
│ │ │ └── versioning
│ │ │ ├── DeclarableRoutingBuilderExt.kt
│ │ │ ├── RouterBuilderExt.kt
│ │ │ ├── Version.kt
│ │ │ ├── VersionRequirements.kt
│ │ │ ├── VersionedRequest.kt
│ │ │ ├── annotations
│ │ │ ├── ExperimentalVersioningApi.kt
│ │ │ ├── InternalVersioningApi.kt
│ │ │ └── VersioningDsl.kt
│ │ │ ├── builders
│ │ │ └── VersioningBuilder.kt
│ │ │ └── preprocessor
│ │ │ └── RequestVersionProvider.kt
│ │ └── jvmTest
│ │ └── kotlin
│ │ └── com
│ │ └── y9vad9
│ │ └── rsocket
│ │ └── router
│ │ └── versioning
│ │ └── test
│ │ └── DeclarableRoutingBuilderExtTest.kt
└── serialization
│ ├── build.gradle.kts
│ └── src
│ └── commonMain
│ └── kotlin
│ └── com
│ └── y9vad9
│ └── rsocket
│ └── router
│ └── versioning
│ └── serialization
│ └── DeclarableRoutingBuilderExt.kt
└── settings.gradle.kts
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: Gradle Publish Release
2 |
3 | on:
4 | release:
5 | types:
6 | - created
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Java
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: '11'
20 | distribution: 'corretto'
21 |
22 | - name: Cache Gradle dependencies
23 | uses: actions/cache@v3
24 | with:
25 | path: ~/.gradle/caches
26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
27 |
28 | - name: Set Gradle executable permissions
29 | run: chmod +x ./gradlew
30 |
31 | - name: Build and Publish
32 | env:
33 | LIB_VERSION: ${{ github.ref_name }}
34 | SSH_DEPLOY_PATH: ${{ secrets.SSH_DEPLOY_PATH }}
35 | SSH_HOST: ${{ secrets.SSH_HOST }}
36 | SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
37 | SSH_USER: ${{ secrets.SSH_USER }}
38 | run: ./gradlew publish
39 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests on PR
2 |
3 | on:
4 | pull_request:
5 | branches: [ "master" ]
6 | release:
7 | types: [ published ]
8 | workflow_dispatch:
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-java@v3
16 | with:
17 | distribution: 'corretto'
18 | java-version: '17'
19 | cache: 'gradle'
20 | - run: ./gradlew check --no-daemon
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | .gradle
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 | # Uncomment the following line in case you need and you don't have the release build type files in your app
20 | # release/
21 |
22 | # Gradle files
23 | .gradle/
24 | build/
25 |
26 | # Local configuration file (sdk path, etc)
27 | local.properties
28 | deploy.properties
29 |
30 | # Proguard folder generated by Eclipse
31 | proguard/
32 |
33 | # Log Files
34 | *.log
35 |
36 | # Android Studio Navigation editor temp files
37 | .navigation/
38 |
39 | # Android Studio captures folder
40 | captures/
41 |
42 | # IntelliJ
43 | *.iml
44 | .idea
45 |
46 | # Keystore files
47 | # Uncomment the following lines if you do not want to check your keystore files in.
48 | #*.jks
49 | #*.keystore
50 |
51 | # External native build folder generated in Android Studio 2.2 and later
52 | .externalNativeBuild
53 | .cxx/
54 |
55 | # Google Services (e.g. APIs or Firebase)
56 | # google-services.json
57 |
58 | # Freeline
59 | freeline.py
60 | freeline/
61 | freeline_project_description.json
62 |
63 | # fastlane
64 | fastlane/report.xml
65 | fastlane/Preview.html
66 | fastlane/screenshots
67 | fastlane/test_output
68 | fastlane/readme.md
69 |
70 | # Version control
71 | vcs.xml
72 |
73 | # lint
74 | lint/intermediates/
75 | lint/generated/
76 | lint/outputs/
77 | lint/tmp/
78 | # lint/reports/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Vadym Yaroshchuk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |  
2 |
3 | # RSocket Router
4 |
5 | `rsocket-kotlin-router` is a customisable library designed to streamline and simplify routing
6 | for RSocket Kotlin server applications. This library offers a typesafe DSL for handling various
7 | routes, serving as a declarative simplified alternative to manual routing that would
8 | otherwise result in long-winded ternary logic or exhaustive when statements.
9 |
10 | Library provides the following features:
11 |
12 | - [Routing Builder](#how-to-use)
13 | - [Interceptors](#Interceptors)
14 | - [Request Versioning](router-versioning)
15 | - [Request Serialization](router-serialization)
16 |
17 | ## How to use
18 |
19 | First of all, you need to implement basic artifacts with routing support. For now, `rsocket-kotlin-router`
20 | is available only at my self-hosted maven:
21 |
22 | ```kotlin
23 | repositories {
24 | maven("https://maven.y9vad9.com")
25 | }
26 |
27 | dependencies {
28 | implementation("com.y9vad9.rsocket.router:router-core:$version")
29 | }
30 | ```
31 |
32 | > For now, it's available for JVM only, but as there is no JVM platform API used,
33 | > new targets will be available [upon your request](https://github.com/y9vad9/rsocket-kotlin-router/issues/new).
34 |
35 | Example of defining RSocket router:
36 |
37 | ```kotlin
38 | val serverRouter = router {
39 | routeSeparator = '.'
40 | routeProvider { metadata: ByteReadPacket? ->
41 | metadata?.read(RoutingMetadata)?.tags?.first()
42 | ?: throw RSocketError.Invalid("No routing metadata was provided")
43 | }
44 |
45 | routing { // this: RoutingBuilder
46 | route("authorization") {
47 | requestResponse("register") { payload: Payload ->
48 | // just 4 example
49 | println(payload.data.readText())
50 | Payload.Empty
51 | }
52 | }
53 | }
54 | }
55 | ```
56 |
57 | See also what else is supported:
58 |
59 |
60 | Interceptors
61 | Interceptors are experimental feature: API can be changed in the future.
62 |
63 | Preprocessors
64 |
65 | Preprocessors are utilities that run before routing feature applies. For cases, when you need to transform input into
66 | something or propagate
67 | values using coroutines – you can
68 | extend [`Preprocessor.Modifier`](https://github.com/y9vad9/rsocket-kotlin-router/blob/2a794e9a8c5d2ac53cb87ea58cfbe4a2ecfa217d/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/interceptors/Interceptor.kt#L39)
69 | or [`Preprocessor.CoroutineContext`](https://github.com/y9vad9/rsocket-kotlin-router/blob/master/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/interceptors/Interceptor.kt#L31).
70 | Here's an example:
71 |
72 | ```kotlin
73 | class MyCoroutineContextElement(val value: String) : CoroutineContext.Element {... }
74 |
75 | @OptIn(ExperimentalInterceptorsApi::class)
76 | class MyCoroutineContextPreprocessor : Preprocessor.CoroutineContext {
77 | override fun intercept(coroutineContext: CoroutineContext, input: Payload): CoroutineContext {
78 | return coroutineContext + MyCoroutineContextElement(value = "smth")
79 | }
80 | }
81 | ```
82 |
83 | Route Interceptors
84 |
85 | In addition to the `Preprocessors`, `rsocket-kotlin-router` also provides API to intercept specific routes:
86 |
87 | ```kotlin
88 | @OptIn(ExperimentalInterceptorsApi::class)
89 | class MyRouteInterceptor : RouteInterceptor.Modifier {
90 | override fun intercept(route: String, input: Payload): Payload {
91 | return Payload.Empty // just for example
92 | }
93 | }
94 | ```
95 |
96 | Installation
97 |
98 | ```kotlin
99 | val serverRouter = router {
100 | preprocessors {
101 | forCoroutineContext(MyCoroutineContextPreprocessor())
102 | }
103 |
104 | sharedInterceptors {
105 | forModification(MyRouteInterceptor())
106 | }
107 | }
108 | ```
109 |
110 |
111 |
112 |
113 | Versioning support
114 |
115 | Here's example of how request versioning looks like:
116 | ```kotlin
117 | requestResponseV("foo") {
118 | version(1) { payload: Payload ->
119 | // handle requests for version "1.0"
120 | Payload.Empty
121 | }
122 | version(2) { payload: Payload ->
123 | // handle requests for version "2.0"
124 | Payload.Empty
125 | }
126 | }
127 | ```
128 |
129 | For details, please refer to the [versioning guide](router-versioning/README.md).
130 |
131 |
132 |
133 | Serialization support
134 |
135 | Here is example of how type-safe requests with serialization/deserialization mechanisms look like:
136 | ```kotlin
137 | requestResponse("register") { foo: Foo ->
138 | return@requestResponse Bar(/* ... */)
139 | }
140 |
141 | // or versioned variant:
142 |
143 | requestResponseV("register") {
144 | version(1) { foo: Foo ->
145 | Bar(/* ... */)
146 | }
147 |
148 | version(2) { qux: Qux ->
149 | FizzBuzz(/* ... */)
150 | }
151 | }
152 | ```
153 |
154 | For details, please refer to the [serialization guide](router-serialization/README.md).
155 |
156 |
157 |
158 | Testing
159 |
160 | `rsocket-kotlin-router` provides ability to test your routes with `router-test` artifact:
161 |
162 | ```kotlin
163 | dependencies {
164 | implementation("com.y9vad9.rsocket.router:router-test:$version")
165 | }
166 | ```
167 |
168 | ```kotlin
169 | @Test
170 | fun testRoutes() {
171 | runBlocking {
172 | val route1 = router.routeAtOrAssert("test")
173 | val route2 = router.routeAtOrAssert("test.subroute")
174 |
175 | route1.assertHasInterceptor()
176 | route2.assertHasInterceptor()
177 |
178 | route2.fireAndForgetOrAssert(buildPayload {
179 | data("test")
180 | })
181 | }
182 | }
183 | ```
184 |
185 | You can refer to the [example](router-core/test/src/jvmTest/kotlin/com/y9vad9/rsocket/router/test/RouterTest.kt) for
186 | more details.
187 |
188 |
189 | ## Bugs and Feedback
190 |
191 | For bugs, questions and discussions please use
192 | the [GitHub Issues](https://github.com/y9vad9/rsocket-kotlin-router/issues).
193 |
194 | ## License
195 |
196 | This library is licensed under [MIT License](LICENSE). Feel free to use, modify, and distribute it for any purpose.
197 |
--------------------------------------------------------------------------------
/build-conventions/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | google()
8 | gradlePluginPortal()
9 | }
10 |
11 | kotlin {
12 | jvmToolchain(11)
13 | }
14 |
15 | dependencies {
16 | api(libs.kotlin.plugin)
17 | api(libs.vanniktech.maven.publish)
18 | }
--------------------------------------------------------------------------------
/build-conventions/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | mavenCentral()
4 | google()
5 | }
6 |
7 | versionCatalogs {
8 | create("libs") {
9 | from(files("../gradle/libs.versions.toml"))
10 | }
11 | }
12 | }
13 |
14 | rootProject.name = "conventions"
--------------------------------------------------------------------------------
/build-conventions/src/main/kotlin/multiplatform-module-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.*
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | kotlin {
9 | jvm()
10 | jvmToolchain(11)
11 |
12 | explicitApi = ExplicitApiMode.Strict
13 | }
14 |
15 | mavenPublishing {
16 | pom {
17 | url.set("https://github.com/y9vad9/rsocket-kotlin-router")
18 | inceptionYear.set("2023")
19 |
20 | licenses {
21 | license {
22 | name.set("The MIT License")
23 | url.set("https://opensource.org/licenses/MIT")
24 | distribution.set("https://opensource.org/licenses/MIT")
25 | }
26 | }
27 |
28 | developers {
29 | developer {
30 | id.set("y9vad9")
31 | name.set("Vadym Yaroshchuk")
32 | url.set("https://github.com/y9vad9/")
33 | }
34 | }
35 |
36 | scm {
37 | url.set("https://github.com/y9vad9/rsocket-kotlin-router")
38 | connection.set("scm:git:git://github.com/y9vad9/rsocket-kotlin-router.git")
39 | developerConnection.set("scm:git:ssh://git@github.com/y9vad9/rsocket-kotlin-router.git")
40 | }
41 |
42 | issueManagement {
43 | system.set("GitHub Issues")
44 | url.set("https://github.com/y9vad9/rsocket-kotlin-router/issues")
45 | }
46 | }
47 | }
48 |
49 | publishing {
50 | repositories {
51 | maven {
52 | name = "y9vad9Maven"
53 |
54 | url = uri(
55 | "sftp://${System.getenv("SSH_HOST")}:22/${System.getenv("SSH_DEPLOY_PATH")}"
56 | )
57 |
58 | credentials {
59 | username = System.getenv("SSH_USER")
60 | password = System.getenv("SSH_PASSWORD")
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.multiplatform) apply false
3 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.mpp.stability.nowarn=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "1.9.10"
3 | kotlinx-coroutines = "1.6.4"
4 | kotlinx-serialization = "1.4.1"
5 | sqldelight = "2.0.0-alpha05"
6 | rsocket = "0.15.4"
7 | ktor = "2.3.0"
8 | mockk = "1.13.5"
9 |
10 | [libraries]
11 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
12 | kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
13 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
14 | kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
15 | kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
16 | rsocket-server = { module = "io.rsocket.kotlin:rsocket-ktor-server", version.ref = "rsocket" }
17 | rsocket-test = { module = "io.rsocket.kotlin:rsocket-test", version.ref = "rsocket" }
18 | rsocket-server-websockets = { module = "io.rsocket.kotlin:rsocket-transport-ktor-websocket-server", version.ref = "rsocket" }
19 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
20 | kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
21 | publish-library = { module = "publish-library:publish-library", version.require = "SNAPSHOT" }
22 | vanniktech-maven-publish = { module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version.require = "0.25.3" }
23 | ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
24 | ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
25 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
26 |
27 | [plugins]
28 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
29 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
30 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
31 | multiplatform-module-convention = { id = "multiplatform-module-convention", version.require = "SNAPSHOT" }
32 | example-module-convention = { id = "example-module-convention", version.require = "SNAPSHOT" }
33 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/y9vad9/rsocket-kotlin-router/38277c9b22f641247f9d615b5a0702fb808137ab/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/router-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | `maven-publish`
4 | }
5 |
6 | group = "com.y9vad9.rsocket.router"
7 | version = System.getenv("LIB_VERSION") ?: "SNAPSHOT"
8 |
9 |
10 | dependencies {
11 | commonMainImplementation(libs.rsocket.server)
12 |
13 | commonMainImplementation(libs.kotlinx.coroutines)
14 | }
15 |
16 | mavenPublishing {
17 | coordinates(
18 | groupId = "com.y9vad9.rsocket.router",
19 | artifactId = "router-core",
20 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing,
21 | )
22 |
23 | pom {
24 | name.set("Router Core")
25 | description.set("Kotlin RSocket library for routing management.")
26 | }
27 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/Route.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NAME_SHADOWING")
2 |
3 | package com.y9vad9.rsocket.router
4 |
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
6 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
7 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
8 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
9 | import io.rsocket.kotlin.RSocketError
10 | import io.rsocket.kotlin.payload.Payload
11 | import kotlinx.coroutines.currentCoroutineContext
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.withContext
14 | import kotlin.coroutines.CoroutineContext
15 |
16 | @OptIn(ExperimentalInterceptorsApi::class, ExperimentalRouterApi::class)
17 | public data class Route internal constructor(
18 | val path: String,
19 | @property:ExperimentalRouterApi
20 | internal val requests: Requests,
21 | @property:ExperimentalInterceptorsApi
22 | val preprocessors: List,
23 | @property:ExperimentalInterceptorsApi
24 | val interceptors: List,
25 | ) {
26 | public suspend fun fireAndForget(payload: Payload) {
27 | processPayload(payload) { payload ->
28 | requests.fireAndForget?.invoke(payload)
29 | ?: throwInvalidRequestOnRoute("fireAndForget")
30 | }
31 | }
32 |
33 | public suspend fun requestResponse(payload: Payload): Payload {
34 | return processPayload(payload) { payload ->
35 | requests.requestResponse?.invoke(payload)
36 | ?: throwInvalidRequestOnRoute("requestResponse")
37 | }
38 | }
39 |
40 | public suspend fun requestStream(payload: Payload): Flow {
41 | return processPayload(payload) { payload ->
42 | requests.requestStream?.invoke(payload)
43 | ?: throwInvalidRequestOnRoute("requestStream")
44 | }
45 | }
46 |
47 | public suspend fun requestChannel(
48 | initPayload: Payload,
49 | payloads: Flow,
50 | ): Flow = processPayload(initPayload) { initialPayload ->
51 | requests.requestChannel?.invoke(initialPayload, payloads)
52 | ?: throwInvalidRequestOnRoute("requestChannel")
53 | }
54 |
55 | private suspend inline fun processPayload(payload: Payload, crossinline block: suspend (Payload) -> R): R {
56 | var coroutineContext: CoroutineContext = currentCoroutineContext()
57 |
58 | val payload =
59 | interceptors.fold(payload) { acc, interceptor ->
60 | when (interceptor) {
61 | is RouteInterceptor.Modifier -> interceptor.intercept(path, acc)
62 | is RouteInterceptor.CoroutineContext -> {
63 | coroutineContext = interceptor.intercept(path, coroutineContext, acc)
64 | acc
65 | }
66 | }
67 | }
68 |
69 | return withContext(coroutineContext) {
70 | block(payload)
71 | }
72 | }
73 |
74 | internal data class Requests(
75 | val fireAndForget: (suspend (payload: Payload) -> Unit)? = null,
76 | val requestResponse: (suspend (payload: Payload) -> Payload)? = null,
77 | val requestStream: (suspend (payload: Payload) -> Flow)? = null,
78 | val requestChannel: (suspend (initPayload: Payload, payloads: Flow) -> Flow)? = null,
79 | )
80 | }
81 |
82 | private fun Route.throwInvalidRequestOnRoute(requestType: String): Nothing =
83 | throw RSocketError.Invalid("No `$requestType` is registered for `$path` route.")
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/Router.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import io.ktor.utils.io.core.*
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
6 | import com.y9vad9.rsocket.router.annotations.InternalRouterApi
7 | import com.y9vad9.rsocket.router.annotations.RouterDsl
8 | import com.y9vad9.rsocket.router.builders.RouterBuilder
9 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
10 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
11 | import io.rsocket.kotlin.*
12 | import io.rsocket.kotlin.payload.Payload
13 | import kotlinx.coroutines.currentCoroutineContext
14 | import kotlinx.coroutines.withContext
15 |
16 | /**
17 | * The RSocket router with all registered routes, configurations, preprocessors
18 | * and interceptors.
19 | */
20 | public interface Router {
21 | /**
22 | * The route separator character used in the application.
23 | *
24 | * This character is used to separate different parts of a route string.
25 | * It allows for hierarchical organization in the navigation or routing of the application.
26 | */
27 | public val routeSeparator: Char
28 |
29 | /**
30 | * The list of preprocessors that is registered for current router.
31 | *
32 | * Preprocessors are always run before any processing from router. They're
33 | * experimental due to considerations of better API.
34 | */
35 | @ExperimentalInterceptorsApi
36 | public val preprocessors: List
37 |
38 | /**
39 | * The list of interceptors that are shared to all the routes.
40 | */
41 | @ExperimentalInterceptorsApi
42 | public val sharedInterceptors: List
43 |
44 | /**
45 | * Retrieves route based on given [path].
46 | */
47 | public fun routeAt(path: String): Route?
48 |
49 | @InternalRouterApi
50 | public suspend fun getRoutePathFromMetadata(metadata: ByteReadPacket?): String
51 | }
52 |
53 | // -- builders --
54 |
55 | @RouterDsl
56 | public fun RSocketRequestHandlerBuilder.router(block: RouterBuilder.() -> Unit): Router {
57 | return router(builder = block).also { router -> router.installOn(this) }
58 | }
59 |
60 | @OptIn(InternalRouterApi::class)
61 | public fun router(builder: RouterBuilder.() -> Unit): Router = RouterBuilder().apply(builder).build()
62 |
63 | /**
64 | * Applies [Router] to given [RSocketRequestHandlerBuilder]. All registered routes are listened.
65 | *
66 | * **Implementation note**: As [Router] does not provide `metadataPush` feature, this function is especially
67 | * useful if you want to additionally provide it for your [RSocket] instance.
68 | */
69 | @OptIn(ExperimentalRouterApi::class, ExperimentalInterceptorsApi::class, InternalRouterApi::class)
70 | public fun Router.installOn(handlerBuilder: RSocketRequestHandlerBuilder): Unit = with(handlerBuilder) {
71 | requestResponse { payload ->
72 | preprocessors.intercepts(payload) {
73 | routeAtOrFail(getRoutePathFromMetadata(it.metadata))
74 | .requestResponse(it)
75 | }
76 | }
77 |
78 | requestStream { payload ->
79 | preprocessors.intercepts(payload) {
80 | routeAtOrFail(getRoutePathFromMetadata(it.metadata))
81 | .requestStream(it)
82 | }
83 | }
84 |
85 | requestChannel { initPayload, payloads ->
86 | preprocessors.intercepts(initPayload) {
87 | routeAtOrFail(getRoutePathFromMetadata(it.metadata))
88 | .requestChannel(it, payloads)
89 | }
90 | }
91 |
92 | fireAndForget { payload ->
93 | preprocessors.intercepts(payload) {
94 | routeAtOrFail(getRoutePathFromMetadata(it.metadata))
95 | .fireAndForget(it)
96 | }
97 | }
98 | }
99 |
100 | @Suppress("UnusedReceiverParameter")
101 | public fun ConnectionAcceptor.installRouter(router: Router): RSocket {
102 | return RSocketRequestHandler {
103 | router.installOn(this)
104 | }
105 | }
106 |
107 |
108 | // -- extensions --
109 |
110 | @ExperimentalRouterApi
111 | @Throws(RSocketError.Invalid::class)
112 | public fun Router.routeAtOrFail(path: String): Route =
113 | routeAt(path) ?: throw RSocketError.Invalid("Route `$path` is not found.")
114 |
115 |
116 | // -- internal implementation --
117 |
118 | internal class RouterImpl @[ExperimentalRouterApi ExperimentalInterceptorsApi] constructor(
119 | override val routeSeparator: Char,
120 | @property:ExperimentalRouterApi
121 | override val preprocessors: List,
122 | @property:ExperimentalRouterApi
123 | override val sharedInterceptors: List,
124 | private val routes: Map,
125 | private var routeProvider: suspend (metadata: ByteReadPacket?) -> String,
126 | ) : Router {
127 | override fun routeAt(path: String): Route? {
128 | return routes[path]
129 | }
130 |
131 | @InternalRouterApi
132 | override suspend fun getRoutePathFromMetadata(metadata: ByteReadPacket?): String {
133 | return routeProvider(metadata)
134 | }
135 | }
136 |
137 | /**
138 | * Applies a list of preprocessors to a payload and then executes a block of code with the processed payload.
139 | *
140 | * @param payload The payload to be processed.
141 | * @param block The block of code to be executed with the processed payload.
142 | * @return The result of executing the block of code.
143 | */
144 | @ExperimentalRouterApi
145 | @ExperimentalInterceptorsApi
146 | public suspend fun List.intercepts(
147 | payload: Payload,
148 | block: suspend (Payload) -> R,
149 | ): R {
150 | var coroutineContext = currentCoroutineContext()
151 |
152 | val processed: Payload = fold(payload) { acc, preprocessor ->
153 | when (preprocessor) {
154 | is Preprocessor.CoroutineContext -> {
155 | coroutineContext = preprocessor.intercept(coroutineContext, acc)
156 | acc
157 | }
158 | is Preprocessor.Modifier -> preprocessor.intercept(acc)
159 | }
160 | }
161 |
162 | return withContext(coroutineContext) {
163 | block(processed)
164 | }
165 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/annotations/ExperimentalInterceptorsApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.annotations
2 |
3 | @RequiresOptIn(message = "Experimental Interceptors API", level = RequiresOptIn.Level.ERROR)
4 | public annotation class ExperimentalInterceptorsApi
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/annotations/ExperimentalRouterApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.annotations
2 |
3 | @RequiresOptIn(message = "Experimental router API", level = RequiresOptIn.Level.ERROR)
4 | public annotation class ExperimentalRouterApi
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/annotations/InternalRouterApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.annotations
2 |
3 | @RequiresOptIn(message = "Internal router API", level = RequiresOptIn.Level.ERROR)
4 | public annotation class InternalRouterApi
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/annotations/RouterDsl.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.annotations
2 |
3 | @DslMarker
4 | public annotation class RouterDsl
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/builders/DeclarableRoutingBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.builders
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.interceptors.builder.RouteInterceptorsBuilder
5 | import io.rsocket.kotlin.payload.Payload
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | /**
9 | * A routing builder with the ability to provide methods at specific routes.
10 | */
11 | public interface DeclarableRoutingBuilder : RoutingBuilder {
12 | /**
13 | * Makes a request to the RSocket server and waits for the response.
14 | *
15 | * @param block A suspend lambda that takes an RSocket instance and a Payload as input and returns a Payload.
16 | * This lambda is responsible for processing the payload and generating the response.
17 | * @return The response Payload from the server.
18 | */
19 | public fun requestResponse(block: suspend (payload: Payload) -> Payload)
20 |
21 | /**
22 | * Makes a stream request to the RSocket with the provided [Payload] and returns a [Flow] of [Payload] as the response.
23 | *
24 | * @param block The block of suspended lambda code to execute, taking the RSocket instance and the input [Payload] as parameters,
25 | * and returning a [Flow] of [Payload].
26 | * The lambda is responsible for handling the stream logic and emitting values to the flow.
27 | * The flow will be automatically cancelable when not required anymore.
28 | *
29 | * @return A [Flow] of [Payload] representing the stream of response payloads received from the server.
30 | *
31 | * @throws Exception if any error occurs during the stream request or handling.
32 | */
33 | public fun requestStream(block: suspend (payload: Payload) -> Flow)
34 |
35 | /**
36 | * Requests a channel within RSocket in current route.
37 | *
38 | * @param block The block of suspended code to be executed, which takes two parameters:
39 | * - initPayload: The initial payload for the channel.
40 | * - payloads: The flow of payloads to be sent to the channel.
41 | * The block should return a flow of payloads received from the channel.
42 | * @return A flow of payloads received from the channel.
43 | */
44 | public fun requestChannel(block: suspend (initPayload: Payload, payloads: Flow) -> Flow)
45 |
46 | /**
47 | * Executes the given [block] in a fire-and-forget manner.
48 | *
49 | * This method is used to send a single request without requiring a response. The [block]
50 | * takes an RSocket instance and a Payload object as parameters, allowing you to perform
51 | * any necessary operations within the block.
52 | *
53 | * @param block the suspend block to be executed in a fire-and-forget manner.
54 | * It takes an RSocket instance and a Payload object as parameters.
55 | * The block is responsible for processing the Payload object accordingly.
56 | */
57 | public fun fireAndForget(block: suspend (payload: Payload) -> Unit)
58 |
59 | /**
60 | * Registers interceptor for current route and its sub-routes.
61 | *
62 | * **Experimental** due to considering better design for API.
63 | */
64 | @ExperimentalInterceptorsApi
65 | public fun interceptors(
66 | builder: RouteInterceptorsBuilder.() -> Unit,
67 | )
68 | }
69 |
70 |
71 | // -- extensions --
72 |
73 | public fun DeclarableRoutingBuilder.requestResponse(
74 | route: String,
75 | block: suspend (payload: Payload) -> Payload
76 | ): Unit = route(route) {
77 | requestResponse(block)
78 | }
79 |
80 | public fun DeclarableRoutingBuilder.requestChannel(
81 | route: String,
82 | block: suspend (initPayload: Payload, payloads: Flow) -> Flow
83 | ): Unit = route(route) {
84 | requestChannel(block)
85 | }
86 |
87 | public fun DeclarableRoutingBuilder.requestStream(
88 | route: String,
89 | block: suspend (payload: Payload) -> Flow
90 | ): Unit = route(route) {
91 | requestStream(block)
92 | }
93 |
94 | public fun DeclarableRoutingBuilder.fireAndForget(
95 | route: String,
96 | block: suspend (payload: Payload) -> Unit
97 | ): Unit = route(route) {
98 | fireAndForget(block)
99 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/builders/RouterBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.builders
2 |
3 | import com.y9vad9.rsocket.router.Router
4 | import com.y9vad9.rsocket.router.RouterImpl
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
6 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
7 | import com.y9vad9.rsocket.router.annotations.InternalRouterApi
8 | import com.y9vad9.rsocket.router.annotations.RouterDsl
9 | import com.y9vad9.rsocket.router.builders.impl.RoutingBuilderScopeImpl
10 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
11 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
12 | import com.y9vad9.rsocket.router.interceptors.builder.PreprocessorsBuilder
13 | import com.y9vad9.rsocket.router.interceptors.builder.RouteInterceptorsBuilder
14 | import io.ktor.utils.io.core.*
15 |
16 | @RouterDsl
17 | public class RouterBuilder @InternalRouterApi constructor() {
18 | @ExperimentalInterceptorsApi
19 | private var preprocessors: List? = null
20 | @ExperimentalInterceptorsApi
21 | private var sharedInterceptors: List? = null
22 |
23 | private var routingConfiguration: (RoutingBuilder.() -> Unit)? = null
24 |
25 | private var routeProvider: (suspend (metadata: ByteReadPacket?) -> String)? = null
26 |
27 | /**
28 | * The routeSeparator variable is used to designate the separator character for routes.
29 | * It is of type Char and can be assigned any character value.
30 | *
31 | * @property routeSeparator The separator character for routes.
32 | *
33 | * @throws IllegalArgumentException if routeSeparator is assigned more than once.
34 | */
35 | public var routeSeparator: Char? = null
36 | set(value) {
37 | require(field == null) { "routeSeparator should be defined once." }
38 | field = value
39 | }
40 |
41 | /**
42 | * Applies preprocessors to the router configuration.
43 | *
44 | * @param builder The lambda function where the preprocessors are configured using the `PreprocessorsBuilder`.
45 | */
46 | @ExperimentalInterceptorsApi
47 | public fun preprocessors(builder: PreprocessorsBuilder.() -> Unit) {
48 | preprocessors = (preprocessors ?: emptyList()) + PreprocessorsBuilder().apply(builder).build()
49 | }
50 |
51 | @ExperimentalInterceptorsApi
52 | public fun sharedInterceptors(builder: RouteInterceptorsBuilder.() -> Unit) {
53 | sharedInterceptors = (sharedInterceptors ?: emptyList()) + RouteInterceptorsBuilder().apply(builder).build()
54 | }
55 |
56 | public fun routeProvider(provider: suspend (metadata: ByteReadPacket?) -> String) {
57 | require(routeProvider == null) { "routeProvider should be defined once." }
58 | routeProvider = provider
59 | }
60 |
61 | public fun routing(
62 | block: RoutingBuilder.() -> Unit,
63 | ) {
64 | require(routingConfiguration == null) { "routing should be defined only once" }
65 | routingConfiguration = block
66 | }
67 |
68 | @OptIn(ExperimentalRouterApi::class, ExperimentalInterceptorsApi::class)
69 | @InternalRouterApi
70 | public fun build(): Router {
71 | require(routingConfiguration != null) { "routing should be defined" }
72 |
73 | val routeSeparator = routeSeparator ?: '.'
74 | val sharedInterceptors = sharedInterceptors.orEmpty()
75 | val preprocessors = preprocessors.orEmpty()
76 | val routeProvider = routeProvider ?: error("Route should be provided.")
77 |
78 | val routingBuilder = RoutingBuilderScopeImpl(
79 | routeSeparator,
80 | sharedInterceptors,
81 | preprocessors,
82 | )
83 |
84 | (routingConfiguration ?: error("routing should be defined")).invoke(routingBuilder)
85 |
86 | return RouterImpl(
87 | routeSeparator,
88 | preprocessors,
89 | sharedInterceptors,
90 | routingBuilder.build(),
91 | routeProvider,
92 | )
93 | }
94 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/builders/RoutingBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.builders
2 |
3 | import com.y9vad9.rsocket.router.Route
4 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
5 | import io.ktor.utils.io.core.*
6 | import io.rsocket.kotlin.ExperimentalMetadataApi
7 | import io.rsocket.kotlin.RSocketError
8 | import io.rsocket.kotlin.RSocketRequestHandlerBuilder
9 | import io.rsocket.kotlin.metadata.RoutingMetadata
10 | import io.rsocket.kotlin.metadata.read
11 | import com.y9vad9.rsocket.router.annotations.RouterDsl
12 | import com.y9vad9.rsocket.router.router
13 | import io.ktor.server.routing.*
14 |
15 | /**
16 | * Interface for building routes for RSocket requests.
17 | *
18 | * **Not stable for inheritance.**
19 | */
20 | @RouterDsl
21 | public interface RoutingBuilder {
22 | /**
23 | * Defines a route for the RSocket request.
24 | *
25 | * @param route The string representation of the route.
26 | * @param block The lambda function to be executed when the route is called.
27 | * It takes a request data ([Payload]) as input and returns response as a [Payload].
28 | */
29 | @RouterDsl
30 | public fun route(route: String, block: DeclarableRoutingBuilder.() -> Unit)
31 | }
32 |
33 | // -- builders --
34 |
35 | /**
36 | * Configures routing for RSocket request handler.
37 | *
38 | * @param routeProvider The route provider function that determines the route based on the incoming request. The default implementation reads the routing metadata from the request and returns the first tag. If no route is provided, it throws an Invalid RSocketError.
39 | * @param routeSeparator The separator character used to separate segments in the route. The default is '.' (dot).
40 | * @param builder The routing configuration builder that defines the request-response, request-stream, request-channel, and fire-and-forget routes.
41 | */
42 | @RouterDsl
43 | public fun RSocketRequestHandlerBuilder.routing(
44 | routeProvider: suspend (metadata: ByteReadPacket?) -> String = @ExperimentalMetadataApi {
45 | it?.read(RoutingMetadata)?.tags?.firstOrNull()
46 | ?: throw RSocketError.Invalid("Route was not provided.")
47 | },
48 | routeSeparator: Char = '.',
49 | builder: RoutingBuilder.() -> Unit,
50 | ) {
51 | router {
52 | this.routeSeparator = routeSeparator
53 | routeProvider(routeProvider)
54 |
55 | routing {
56 | builder()
57 | }
58 | }
59 | }
60 |
61 |
62 |
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/builders/impl/DeclarableRoutingBuilderScopeImpl.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.builders.impl
2 |
3 | import com.y9vad9.rsocket.router.Route
4 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
5 | import com.y9vad9.rsocket.router.builders.DeclarableRoutingBuilder
6 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
7 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
8 | import com.y9vad9.rsocket.router.interceptors.builder.RouteInterceptorsBuilder
9 | import io.rsocket.kotlin.payload.Payload
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @OptIn(ExperimentalInterceptorsApi::class)
13 | internal class DeclarableRoutingBuilderScopeImpl(
14 | private val path: String,
15 | private val separator: Char,
16 | private val inheritedInterceptors: List,
17 | private val preprocessors: List,
18 | ) : DeclarableRoutingBuilder {
19 | private var currentInterceptors: List? = null
20 | private var requests = Route.Requests()
21 | private val subRoutes = mutableMapOf()
22 |
23 | override fun requestResponse(block: suspend (payload: Payload) -> Payload) {
24 | require(requests.requestResponse == null) { "Request-Response is already defined." }
25 | requests = requests.copy(
26 | requestResponse = block,
27 | )
28 | }
29 |
30 | override fun requestStream(block: suspend (payload: Payload) -> Flow) {
31 | require(requests.requestResponse == null) { "Request-Stream is already defined." }
32 | requests = requests.copy(
33 | requestStream = block,
34 | )
35 | }
36 |
37 | override fun requestChannel(block: suspend (initPayload: Payload, payloads: Flow) -> Flow) {
38 | require(requests.requestChannel == null) { "Request-Channel is already defined." }
39 | requests = requests.copy(
40 | requestChannel = block,
41 | )
42 | }
43 |
44 | override fun fireAndForget(block: suspend (payload: Payload) -> Unit) {
45 | require(requests.fireAndForget == null) { "Fire-and-Forget is already defined." }
46 | requests = requests.copy(
47 | fireAndForget = block,
48 | )
49 | }
50 |
51 | override fun interceptors(builder: RouteInterceptorsBuilder.() -> Unit) {
52 | require(currentInterceptors == null) { "interceptors should be defined only once." }
53 | currentInterceptors = RouteInterceptorsBuilder().apply(builder).build()
54 | }
55 |
56 | override fun route(route: String, block: DeclarableRoutingBuilder.() -> Unit) {
57 | subRoutes += DeclarableRoutingBuilderScopeImpl(
58 | path = "${path}${separator}${route}",
59 | separator = separator,
60 | inheritedInterceptors = inheritedInterceptors + currentInterceptors.orEmpty(),
61 | preprocessors = preprocessors,
62 | ).apply(block).build()
63 | }
64 |
65 |
66 | fun build(): Map = buildMap {
67 | put(path, Route(path, requests, preprocessors, inheritedInterceptors))
68 | putAll(subRoutes)
69 | }
70 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/builders/impl/RoutingBuilderScopeImpl.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.builders.impl
2 |
3 | import com.y9vad9.rsocket.router.Route
4 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
6 | import com.y9vad9.rsocket.router.builders.DeclarableRoutingBuilder
7 | import com.y9vad9.rsocket.router.builders.RoutingBuilder
8 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
9 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
10 |
11 | @OptIn(ExperimentalInterceptorsApi::class)
12 | internal class RoutingBuilderScopeImpl constructor(
13 | private val separator: Char,
14 | private val sharedInterceptors: List,
15 | private val preprocessors: List,
16 | ) : RoutingBuilder {
17 | private val subRoutes = mutableMapOf()
18 |
19 | override fun route(route: String, block: DeclarableRoutingBuilder.() -> Unit) {
20 | subRoutes += DeclarableRoutingBuilderScopeImpl(
21 | path = route,
22 | separator = separator,
23 | inheritedInterceptors = sharedInterceptors,
24 | preprocessors = preprocessors,
25 | ).apply(block).build()
26 | }
27 |
28 | fun build(): Map = subRoutes.toMap()
29 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/interceptors/Interceptor.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.interceptors
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import io.rsocket.kotlin.payload.Payload
5 |
6 | /**
7 | * An interceptor for the RSocket library used by Router.
8 | *
9 | * @param The type of the request.
10 | * @param The return type of the interceptor.
11 | */
12 | @ExperimentalInterceptorsApi
13 | public sealed interface Interceptor
14 |
15 | /**
16 | * This interface represents a preprocessor, which is an interceptor for intercepting requests before the route feature.
17 | */
18 | @ExperimentalInterceptorsApi
19 | public sealed interface Preprocessor : Interceptor {
20 | /**
21 | * A coroutine context, which is responsible for preprocessing payloads
22 | * and intercepting coroutine execution.
23 | *
24 | * **Incoming payload should be copied itself if needed**. By default,
25 | * it's not copied after / before Preprocessor is called.
26 | */
27 | public fun interface CoroutineContext : Preprocessor {
28 | public fun intercept(coroutineContext: kotlin.coroutines.CoroutineContext, input: Payload): kotlin.coroutines.CoroutineContext
29 | }
30 |
31 | /**
32 | * This interface represents a modifier that can be used to preprocess payloads.
33 | * @see Preprocessor
34 | */
35 | public fun interface Modifier : Preprocessor {
36 | public fun intercept(input: Payload): Payload
37 | }
38 | }
39 |
40 |
41 | /**
42 | * Interceptr that works after route feature.
43 | */
44 | @ExperimentalInterceptorsApi
45 | public sealed interface RouteInterceptor : Interceptor {
46 |
47 | /**
48 | * The CoroutineContext interface is used to propagate values to request execution.
49 | * **Incoming payload should be copied itself if needed**. By default,
50 | * it's not copied after / before Preprocessor is called.
51 | */
52 | public fun interface CoroutineContext : RouteInterceptor {
53 | public fun intercept(
54 | route: String,
55 | coroutineContext: kotlin.coroutines.CoroutineContext,
56 | input: Payload,
57 | ): kotlin.coroutines.CoroutineContext
58 | }
59 |
60 |
61 | /**
62 | * The `Modifier` interface is used to modify an incoming payload with a route.
63 | * It is defined as a route interceptor and extends the `RouteInterceptor` interface.
64 | *
65 | * @param The type of payload that contains information about the route.
66 | * @param The type of payload to be modified.
67 | */
68 | public fun interface Modifier : RouteInterceptor {
69 | public fun intercept(route: String, input: Payload): Payload
70 | }
71 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/interceptors/builder/PreprocessorsBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.interceptors.builder
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
5 |
6 | @ExperimentalInterceptorsApi
7 | public class PreprocessorsBuilder internal constructor() {
8 | private val preprocessors = mutableListOf()
9 |
10 | public fun forCoroutineContext(preprocessor: Preprocessor.CoroutineContext) {
11 | preprocessors += preprocessor
12 | }
13 |
14 | public fun forModification(preprocessor: Preprocessor.Modifier) {
15 | preprocessors += preprocessor
16 | }
17 |
18 | internal fun build(): List = preprocessors.toList()
19 | }
--------------------------------------------------------------------------------
/router-core/src/commonMain/kotlin/com.y9vad9.rsocket.router/interceptors/builder/RouteInterceptorsBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.interceptors.builder
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
5 |
6 | @ExperimentalInterceptorsApi
7 | public class RouteInterceptorsBuilder internal constructor() {
8 | private val interceptors = mutableListOf()
9 |
10 | public fun forCoroutineContext(interceptor: RouteInterceptor.CoroutineContext) {
11 | interceptors += interceptor
12 | }
13 |
14 | public fun forModification(interceptor: RouteInterceptor.Modifier) {
15 | interceptors += interceptor
16 | }
17 |
18 | internal fun build(): List = interceptors.toList()
19 | }
--------------------------------------------------------------------------------
/router-core/test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | group = "com.y9vad9.rsocket.router"
6 | version = System.getenv("LIB_VERSION") ?: "SNAPSHOT"
7 |
8 | dependencies {
9 | commonMainImplementation(libs.rsocket.server)
10 |
11 | commonMainImplementation(projects.routerCore)
12 |
13 | commonMainImplementation(libs.kotlinx.coroutines)
14 | commonTestImplementation(libs.kotlin.test)
15 | }
16 |
17 | mavenPublishing {
18 | coordinates(
19 | groupId = "com.y9vad9.rsocket.router",
20 | artifactId = "router-test",
21 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing,
22 | )
23 |
24 | pom {
25 | name.set("Router Test")
26 | description.set("Library for testing rsocket routes.")
27 | }
28 | }
--------------------------------------------------------------------------------
/router-core/test/src/commonMain/kotlin/com/y9vad9/rsocket/router/test/RouterExt.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.test
2 |
3 | import com.y9vad9.rsocket.router.Router
4 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
6 | import com.y9vad9.rsocket.router.intercepts
7 | import io.rsocket.kotlin.payload.Payload
8 |
9 | /**
10 | * Applies a list of preprocessors available in router to a payload and then executes a block of code with the
11 | * processed payload.
12 | *
13 | * @param payload The payload to be preprocessed.
14 | * @param block The block of code to be executed after preprocessing the payload.
15 | * @return The result of executing the block.
16 | */
17 | @OptIn(ExperimentalRouterApi::class)
18 | @ExperimentalInterceptorsApi
19 | public suspend fun Router.preprocess(payload: Payload, block: suspend (Payload) -> R): R =
20 | preprocessors.intercepts(payload, block)
--------------------------------------------------------------------------------
/router-core/test/src/commonMain/kotlin/com/y9vad9/rsocket/router/test/Validators.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.test
2 |
3 | import com.y9vad9.rsocket.router.Route
4 | import com.y9vad9.rsocket.router.Router
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
6 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
7 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
8 | import io.ktor.util.reflect.*
9 | import io.rsocket.kotlin.payload.Payload
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlin.reflect.KClass
12 |
13 | @ExperimentalInterceptorsApi
14 | public inline fun Route.assertHasInterceptor(): Unit =
15 | assertHasInterceptor(T::class)
16 |
17 | @ExperimentalInterceptorsApi
18 | public fun Route.assertHasInterceptor(ofClass: KClass) {
19 | interceptors.firstOrNull { it.instanceOf(ofClass) }
20 | ?: throw AssertionError("Required interceptor `${ofClass.simpleName}` is not found for `$path` route.")
21 | }
22 |
23 | @ExperimentalInterceptorsApi
24 | public inline fun Route.assertHasPreprocessor(): Unit =
25 | assertHasPreprocessor(T::class)
26 |
27 | @ExperimentalInterceptorsApi
28 | public fun Route.assertHasPreprocessor(ofClass: KClass) {
29 | preprocessors.firstOrNull { it.instanceOf(ofClass) }
30 | ?: throw AssertionError(
31 | "Required preprocessor `${ofClass.simpleName}` is not found for `$path` route."
32 | )
33 | }
34 |
35 | public suspend fun Route.fireAndForgetOrAssert(payload: Payload): Unit = try {
36 | fireAndForget(payload)
37 | } catch (e: Throwable) {
38 | throw AssertionError("Failed to execute fire-and-forget method on `$path` route.", e)
39 | }
40 |
41 | public suspend fun Route.requestResponseOrAssert(payload: Payload): Payload = try {
42 | requestResponse(payload)
43 | } catch (e: Throwable) {
44 | throw AssertionError("Failed to execute request-response method on `$path` route.", e)
45 | }
46 |
47 | public suspend fun Route.requestStreamOrAssert(payload: Payload): Flow = try {
48 | requestStream(payload)
49 | } catch (e: Throwable) {
50 | throw AssertionError("Failed to execute request-stream method on `$path` route.", e)
51 | }
52 |
53 | public suspend fun Route.requestChannelOrAssert(
54 | initPayload: Payload,
55 | payloads: Flow,
56 | ): Flow = try {
57 | requestChannel(initPayload, payloads)
58 | } catch (e: Throwable) {
59 | throw AssertionError("Failed to execute request-channel method on `$path` route.", e)
60 | }
61 |
62 | public fun Router.routeAtOrAssert(path: String): Route = routeAt(path)
63 | ?: throw AssertionError("Route at `$path` path is not found.")
64 |
65 | public fun Router.assertHasRoute(path: String) {
66 | routeAtOrAssert(path)
67 | }
--------------------------------------------------------------------------------
/router-core/test/src/jvmTest/kotlin/com/y9vad9/rsocket/router/test/RouterTest.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalInterceptorsApi::class)
2 |
3 | package com.y9vad9.rsocket.router.test
4 |
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
6 | import com.y9vad9.rsocket.router.annotations.ExperimentalRouterApi
7 | import com.y9vad9.rsocket.router.interceptors.RouteInterceptor
8 | import com.y9vad9.rsocket.router.router
9 | import io.rsocket.kotlin.payload.Payload
10 | import io.rsocket.kotlin.payload.buildPayload
11 | import io.rsocket.kotlin.payload.data
12 | import kotlinx.coroutines.currentCoroutineContext
13 | import kotlinx.coroutines.runBlocking
14 | import kotlin.coroutines.CoroutineContext
15 | import kotlin.test.Test
16 | import kotlin.test.assertEquals
17 | import kotlin.test.assertNotNull
18 |
19 | class RouterTest {
20 | private class MyInterceptor : RouteInterceptor.CoroutineContext {
21 |
22 | data class SomeCoroutineContextElement(
23 | val value: String,
24 | ) : CoroutineContext.Element {
25 | companion object Key : CoroutineContext.Key
26 |
27 | override val key: CoroutineContext.Key<*>
28 | get() = Key
29 | }
30 | override fun intercept(route: String, coroutineContext: CoroutineContext, input: Payload): CoroutineContext {
31 | return coroutineContext + SomeCoroutineContextElement("test")
32 | }
33 | }
34 |
35 | private val router = router {
36 | routeSeparator = '.'
37 | sharedInterceptors {
38 | forCoroutineContext(MyInterceptor())
39 | }
40 |
41 | routeProvider { error("Stub!") }
42 |
43 | routing {
44 | route("test") {
45 | route("subroute") {
46 | fireAndForget {
47 | assertEquals(expected = "test", actual = it.data.readText())
48 | assertNotNull(currentCoroutineContext()[MyInterceptor.SomeCoroutineContextElement]?.value)
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | @Test
56 | fun testRoutes() {
57 | runBlocking {
58 | val route1 = router.routeAtOrAssert("test")
59 | val route2 = router.routeAtOrAssert("test.subroute")
60 |
61 | route1.assertHasInterceptor()
62 | route2.assertHasInterceptor()
63 |
64 | route2.fireAndForgetOrAssert(buildPayload { data("test")})
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/router-serialization/README.md:
--------------------------------------------------------------------------------
1 | # Requests Serialization
2 |
3 | Often, we have a type-safe contract specifying what we accept and what we return in requests. Serializing this data can be challenging, especially when dealing with different formats or migrating to a new one. Even if it's not the case, defining your own wrappers or extensions from scratch takes time and, as mentioned before, can lead to potential problems in the future. That's why `rsocket-kotlin-router` provides a ready-to-use pragmatic serialization system.
4 |
5 | > **Warning**
6 | > This feature is experimental, and migration steps might be required in the future.
7 |
8 | ## How to Use
9 |
10 | ### Implementation
11 |
12 | First, add the necessary dependencies:
13 |
14 | ```kotlin
15 | dependencies {
16 | implementation("com.y9vad9.rsocket.router:router-serialization-core:$version")
17 |
18 | // for JSON support
19 | implementation("com.y9vad9.rsocket.router:router-serialization-json:$version")
20 | // for ProtoBuf support
21 | implementation("com.y9vad9.rsocket.router:router-serialization-protobuf:$version")
22 | // for Cbor support
23 | implementation("com.y9vad9.rsocket.router:router-serialization-cbor:$version")
24 | }
25 | ```
26 | ### Installation
27 | To add serialization to your requests, install the required ContentSerializer in your router. For example, using JsonContentSerializer:
28 | ```kotlin
29 | val router = router {
30 | // ...
31 | serialization { JsonContentSerializer() }
32 | // ...
33 | }
34 | ```
35 | ### Usage
36 | You can use the bundled extensions as follows:
37 | ```kotlin
38 | routing {
39 | route("authorization") {
40 | requestResponse("register") { foo: Foo ->
41 | return@requestResponse Bar(/* ... */)
42 | }
43 | // other types of the requests have the same extensions
44 | }
45 | }
46 | ```
47 | ### Custom formats
48 | To add support for other existing formats, you can simply extend `ContentSerializer`. You can take a look at
49 | [`JsonContentSerializer`](json/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/json/JsonContentSerializer.kt)
50 | as an example.
--------------------------------------------------------------------------------
/router-serialization/cbor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 | commonMainImplementation(libs.kotlinx.serialization.cbor)
8 |
9 | commonMainImplementation(projects.routerSerialization.core)
10 | commonMainImplementation(projects.routerCore)
11 | }
12 |
13 | mavenPublishing {
14 | coordinates(
15 | groupId = "com.y9vad9.rsocket.router",
16 | artifactId = "router-serialization-cbor",
17 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
18 | )
19 |
20 | pom {
21 | name.set("Router Serialization (Cbor)")
22 | description.set(
23 | """
24 | Kotlin RSocket library for type-safe serializable routing. Provides Cbor implementation of `ContentSerializer`.
25 | """.trimIndent()
26 | )
27 | }
28 | }
--------------------------------------------------------------------------------
/router-serialization/cbor/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/json/CborContentSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.json
2 |
3 | import com.y9vad9.rsocket.router.serialization.ContentSerializer
4 | import io.ktor.util.*
5 | import io.ktor.utils.io.core.*
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.cbor.Cbor
9 | import kotlinx.serialization.serializer
10 | import kotlinx.serialization.serializerOrNull
11 | import kotlin.reflect.KType
12 |
13 | /**
14 | * A content serializer that uses Cbor format for encoding and decoding data.
15 | *
16 | * @param cbor The Cbor object to use for serialization and deserialization.
17 | */
18 | @ExperimentalSerializationApi
19 | public class CborContentSerializer(private val cbor: Cbor = Cbor) : ContentSerializer {
20 | public companion object Default : ContentSerializer by CborContentSerializer()
21 |
22 | /**
23 | * Decodes a serialized object from a ByteReadPacket using Cbor serialization.
24 | *
25 | * @param kType The KType representing the type of the object to be decoded.
26 | * @param packet The ByteReadPacket containing the serialized object data.
27 | * @return The deserialized object of type T.
28 | */
29 | @Suppress("UNCHECKED_CAST")
30 | @OptIn(ExperimentalSerializationApi::class)
31 | override fun decode(kType: KType, packet: ByteReadPacket): T {
32 | return cbor.decodeFromByteArray(
33 | (cbor.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
34 | packet.readBytes(),
35 | )
36 | }
37 |
38 | /**
39 | * Encodes the given value of type T to a ByteReadPacket using the Cbor serialization.
40 | *
41 | * @param kType The Kotlin type of the value.
42 | * @param value The value to encode.
43 | * @return The encoded value as a ByteReadPacket.
44 | */
45 | @Suppress("UNCHECKED_CAST")
46 | override fun encode(kType: KType, value: T): ByteReadPacket {
47 | return ByteReadPacket(
48 | cbor.encodeToByteArray(
49 | (cbor.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
50 | value,
51 | )
52 | )
53 | }
54 | }
--------------------------------------------------------------------------------
/router-serialization/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 |
8 | commonMainImplementation(projects.routerCore)
9 | }
10 |
11 | mavenPublishing {
12 | coordinates(
13 | groupId = "com.y9vad9.rsocket.router",
14 | artifactId = "router-serialization-core",
15 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
16 | )
17 |
18 | pom {
19 | name.set("Router Serialization Core")
20 | description.set(
21 | """
22 | Kotlin RSocket library for type-safe serializable routing. Provides extensions for routing builder and
23 | abstraction to serialize/deserialize data.
24 | """.trimIndent()
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/ContentSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization
2 |
3 | import io.ktor.utils.io.core.*
4 | import kotlin.reflect.KClass
5 | import kotlin.reflect.KType
6 | import kotlin.reflect.typeOf
7 |
8 | public interface ContentSerializer {
9 | /**
10 | * Serializes the given [packet] into a specific type [T].
11 | *
12 | * @param kClass The [KClass] representing the type [T].
13 | * @param packet The [ByteReadPacket] to be serialized.
14 | * @throws SerializationException if an error occurs during serialization.
15 | */
16 | public fun decode(kType: KType, packet: ByteReadPacket): T
17 |
18 | /**
19 | * Deserializes the given value of type T into a ByteReadPacket.
20 | *
21 | * @param kClass The class of the value to be deserialized.
22 | * @param value The value to be deserialized.
23 | * @return The deserialized value as a ByteReadPacket.
24 | */
25 | public fun encode(kType: KType, value: T): ByteReadPacket
26 | }
27 |
28 | /**
29 | * Reified version of [ContentSerializer.decode]. Uses the reified type [T] to automatically infer its KClass.
30 | *
31 | * @param packet The [ByteReadPacket] to be serialized.
32 | */
33 | public inline fun ContentSerializer.decode(packet: ByteReadPacket): T {
34 | return decode(typeOf(), packet)
35 | }
36 |
37 | /**
38 | * Reified version of [ContentSerializer.encode]. Uses the reified type [T] to automatically infer its KClass.
39 | *
40 | * @param value The value to be deserialized.
41 | * @return The deserialized value as a ByteReadPacket.
42 | */
43 | public inline fun ContentSerializer.encode(value: T): ByteReadPacket {
44 | return encode(typeOf(), value)
45 | }
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/DeclarableRoutingBuilderExt.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(
2 | ExperimentalInterceptorsApi::class,
3 | InternalRouterSerializationApi::class,
4 | )
5 |
6 | package com.y9vad9.rsocket.router.serialization
7 |
8 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
9 | import com.y9vad9.rsocket.router.builders.DeclarableRoutingBuilder
10 | import com.y9vad9.rsocket.router.serialization.annotations.InternalRouterSerializationApi
11 | import com.y9vad9.rsocket.router.serialization.preprocessor.SerializationProvider
12 | import io.rsocket.kotlin.payload.Payload
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.map
15 | import kotlin.reflect.typeOf
16 |
17 | /**
18 | * Executes a request-response operation with the given payload.
19 | *
20 | * @param T the type of the input payload.
21 | * @param R the type of the output payload.
22 | * @param block the suspend lambda that performs the request-response operation.
23 | */
24 | public inline fun DeclarableRoutingBuilder.requestResponse(
25 | crossinline block: suspend (T) -> R,
26 | ): Unit = requestResponse { payload ->
27 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
28 |
29 | block(contentSerializer.decode(typeOf(), payload.data))
30 | .let { result -> contentSerializer.encode(typeOf(), result) }
31 | .let { data -> Payload(data = data) }
32 | }
33 |
34 | /**
35 | * Executes a streaming request with the given payload.
36 | *
37 | * @param T the type of the payload to be decoded.
38 | * @param R the type of the result to be encoded.
39 | * @param block the suspend lambda function that takes the decoded payload and returns a Flow of results.
40 | *
41 | * @return Unit
42 | */
43 | public inline fun DeclarableRoutingBuilder.requestStream(
44 | crossinline block: suspend (T) -> Flow,
45 | ): Unit = requestStream { payload ->
46 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
47 |
48 | block(contentSerializer.decode(typeOf(), payload.data))
49 | .map { result -> Payload(data = contentSerializer.encode(typeOf(), result)) }
50 | }
51 |
52 | /**
53 | * Executes a given suspend block without expecting any return value.
54 | *
55 | * @param block The suspend block to be executed.
56 | * @param T The type of the payload to be passed to the block.
57 | */
58 | public inline fun DeclarableRoutingBuilder.fireAndForget(
59 | crossinline block: suspend (T) -> Unit,
60 | ): Unit = fireAndForget { payload ->
61 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
62 |
63 | block(contentSerializer.decode(payload.data))
64 | }
65 |
66 | /**
67 | * Sends a request channel to the router and provides a response channel.
68 | * This method is used to send a series of request elements (`T`) to the router,
69 | * receive a series of response elements (`R`) from the router, and complete the channel.
70 | *
71 | * @param T the type of the initial request element and subsequent request elements
72 | * @param R the type of the response elements
73 | * @param block the suspending lambda function that processes the incoming request elements and returns the response elements
74 | */
75 | public inline fun DeclarableRoutingBuilder.requestChannel(
76 | crossinline block: suspend (initial: T, Flow) -> Flow,
77 | ): Unit = requestChannel { initial: Payload, payloads: Flow ->
78 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
79 |
80 | val init = contentSerializer.decode(initial.data)
81 | val mappedPayloads: Flow = payloads.map { contentSerializer.decode(it.data) }
82 |
83 | block(init, mappedPayloads)
84 | .map { result -> Payload(data = contentSerializer.encode(result)) }
85 | }
86 |
87 |
88 | public inline fun DeclarableRoutingBuilder.requestResponse(
89 | path: String,
90 | crossinline block: suspend (T) -> R,
91 | ): Unit = route(path) {
92 | requestResponse(block)
93 | }
94 |
95 | public inline fun DeclarableRoutingBuilder.requestStream(
96 | path: String,
97 | crossinline block: suspend (T) -> Flow,
98 | ): Unit = route(path) {
99 | requestStream(block)
100 | }
101 |
102 | public inline fun DeclarableRoutingBuilder.fireAndForget(
103 | path: String,
104 | crossinline block: suspend (T) -> Unit,
105 | ): Unit = route(path) {
106 | fireAndForget(block)
107 | }
108 |
109 | public inline fun DeclarableRoutingBuilder.requestChannel(
110 | path: String,
111 | crossinline block: suspend (initial: T, Flow) -> Flow,
112 | ): Unit = route(path) {
113 | requestChannel(block)
114 | }
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/annotations/ExperimentalRouterSerializationApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.annotations
2 |
3 | @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
4 | public annotation class ExperimentalRouterSerializationApi
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/annotations/InternalRouterSerializationApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.annotations
2 |
3 | @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
4 | public annotation class InternalRouterSerializationApi
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/context/SerializationContext.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.context
2 |
3 | import com.y9vad9.rsocket.router.serialization.ContentSerializer
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal data class SerializationContext(
7 | val contentSerializer: ContentSerializer,
8 | ) : CoroutineContext.Element {
9 | override val key: CoroutineContext.Key<*> = Key
10 |
11 | companion object Key : CoroutineContext.Key
12 | }
--------------------------------------------------------------------------------
/router-serialization/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/preprocessor/SerializationProvider.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.preprocessor
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.builders.RouterBuilder
5 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
6 | import com.y9vad9.rsocket.router.serialization.ContentSerializer
7 | import com.y9vad9.rsocket.router.serialization.annotations.ExperimentalRouterSerializationApi
8 | import com.y9vad9.rsocket.router.serialization.annotations.InternalRouterSerializationApi
9 | import com.y9vad9.rsocket.router.serialization.context.SerializationContext
10 | import io.rsocket.kotlin.payload.Payload
11 | import kotlin.coroutines.CoroutineContext
12 | import kotlin.coroutines.coroutineContext
13 |
14 | @ExperimentalInterceptorsApi
15 | public abstract class SerializationProvider : Preprocessor.CoroutineContext {
16 | public companion object {
17 | /**
18 | * Retrieves the [ContentSerializer] from the coroutine context.
19 | *
20 | * **API note**:
21 | * You shouldn't call this function yourself unless you use it to define your own extensions
22 | * that should be dependent on it.
23 | *
24 | * **Failure note**:
25 | * 1) if you didn't call it yourself and, probably you need to register `SerializationProvider` by
26 | * putting it in the preprocessors or by using `serialization` function in `RoutingBuilder`.
27 | * 2) if function wasn't called by you intentionally and `SerializationProvider` is already
28 | * registered, but inside `test` artifact you should provide content serializer to context using `asContextElement`.
29 | *
30 | * **Testing note**:
31 | * 1) Before testing specific route take in count that `Route` does not run preprocessors, you should
32 | * do it manually using [com.y9vad9.rsocket.router.test.preprocess] or [com.y9vad9.rsocket.router.intercepts]
33 | * functions.
34 | *
35 | * @return The [ContentSerializer] obtained from the coroutine context.
36 | * @throws IllegalStateException If the [ContentSerializer] was not provided or the method was called from
37 | * an illegal context.
38 | */
39 | @InternalRouterSerializationApi
40 | public suspend fun getFromCoroutineContext(): ContentSerializer {
41 | return coroutineContext[SerializationContext]?.contentSerializer
42 | ?: error("ContentSerializer wasn't provided or call happened from illegal context")
43 | }
44 |
45 | @ExperimentalRouterSerializationApi
46 | public suspend fun asContextElement(serializer: ContentSerializer): CoroutineContext =
47 | coroutineContext + SerializationContext(serializer)
48 | }
49 |
50 | public abstract fun provide(coroutineContext: CoroutineContext, payload: Payload): ContentSerializer
51 |
52 | final override fun intercept(coroutineContext: CoroutineContext, input: Payload): CoroutineContext {
53 | return coroutineContext + SerializationContext(provide(coroutineContext, input))
54 | }
55 | }
56 |
57 | public fun RouterBuilder.serialization(block: () -> ContentSerializer) {
58 | serialization { _, _ -> block() }
59 | }
60 |
61 | @OptIn(ExperimentalInterceptorsApi::class)
62 | public fun RouterBuilder.serialization(block: (CoroutineContext, Payload) -> ContentSerializer) {
63 | preprocessors {
64 | forCoroutineContext(
65 | object : SerializationProvider() {
66 | override fun provide(
67 | coroutineContext: CoroutineContext,
68 | payload: Payload,
69 | ): ContentSerializer {
70 | return block(coroutineContext, payload)
71 | }
72 | }
73 | )
74 | }
75 | }
--------------------------------------------------------------------------------
/router-serialization/json/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 | commonMainImplementation(libs.kotlinx.serialization.json)
8 |
9 | commonMainImplementation(projects.routerSerialization.core)
10 | commonMainImplementation(projects.routerCore)
11 | }
12 |
13 | mavenPublishing {
14 | coordinates(
15 | groupId = "com.y9vad9.rsocket.router",
16 | artifactId = "router-serialization-json",
17 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
18 | )
19 |
20 | pom {
21 | name.set("Router Serialization (Json)")
22 | description.set(
23 | """
24 | Kotlin RSocket library for type-safe serializable routing. Provides JSON implementation of `ContentSerializer`.
25 | """.trimIndent()
26 | )
27 | }
28 | }
--------------------------------------------------------------------------------
/router-serialization/json/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/json/JsonContentSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.json
2 |
3 | import com.y9vad9.rsocket.router.serialization.ContentSerializer
4 | import io.ktor.util.*
5 | import io.ktor.utils.io.core.*
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.json.Json
9 | import kotlinx.serialization.json.decodeFromStream
10 | import kotlinx.serialization.serializer
11 | import kotlinx.serialization.serializerOrNull
12 | import kotlin.reflect.KType
13 |
14 | /**
15 | * A content serializer that uses JSON format for encoding and decoding data.
16 | *
17 | * @property json The JSON object to use for serialization and deserialization.
18 | */
19 | public class JsonContentSerializer(private val json: Json = Json) : ContentSerializer {
20 | public companion object Default : ContentSerializer by JsonContentSerializer()
21 | /**
22 | * Decodes a serialized object from a ByteReadPacket using JSON serialization.
23 | *
24 | * @param kType The KType representing the type of the object to be decoded.
25 | * @param packet The ByteReadPacket containing the serialized object data.
26 | * @return The deserialized object of type T.
27 | */
28 | @Suppress("UNCHECKED_CAST")
29 | @OptIn(ExperimentalSerializationApi::class)
30 | override fun decode(kType: KType, packet: ByteReadPacket): T {
31 | return json.decodeFromStream(
32 | (json.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
33 | packet.asStream()
34 | )
35 | }
36 |
37 | /**
38 | * Encodes the given value of type T to a ByteReadPacket using the JSON serialization.
39 | *
40 | * @param kType The Kotlin type of the value.
41 | * @param value The value to encode.
42 | * @return The encoded value as a ByteReadPacket.
43 | */
44 | @Suppress("UNCHECKED_CAST")
45 | override fun encode(kType: KType, value: T): ByteReadPacket {
46 | return ByteReadPacket(
47 | json.encodeToString(
48 | (json.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
49 | value,
50 | ).toByteArray()
51 | )
52 | }
53 | }
--------------------------------------------------------------------------------
/router-serialization/protobuf/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 | commonMainImplementation(libs.kotlinx.serialization.protobuf)
8 |
9 | commonMainImplementation(projects.routerSerialization.core)
10 | commonMainImplementation(projects.routerCore)
11 | }
12 |
13 | mavenPublishing {
14 | coordinates(
15 | groupId = "com.y9vad9.rsocket.router",
16 | artifactId = "router-serialization-protobuf",
17 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
18 | )
19 |
20 | pom {
21 | name.set("Router Serialization (ProtoBuf)")
22 | description.set(
23 | """
24 | Kotlin RSocket library for type-safe serializable routing. Provides ProtoBuf implementation of `ContentSerializer`.
25 | """.trimIndent()
26 | )
27 | }
28 | }
--------------------------------------------------------------------------------
/router-serialization/protobuf/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/json/ProtobufContentSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization.json
2 |
3 | import com.y9vad9.rsocket.router.serialization.ContentSerializer
4 | import io.ktor.util.*
5 | import io.ktor.utils.io.core.*
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.protobuf.ProtoBuf
9 | import kotlinx.serialization.serializer
10 | import kotlinx.serialization.serializerOrNull
11 | import kotlin.reflect.KType
12 |
13 | /**
14 | * A content serializer that uses ProtoBuf format for encoding and decoding data.
15 | *
16 | * @param protoBuf The ProtoBuf object to use for serialization and deserialization.
17 | */
18 | @ExperimentalSerializationApi
19 | public class ProtobufContentSerializer(private val protoBuf: ProtoBuf = ProtoBuf) : ContentSerializer {
20 | public companion object Default : ContentSerializer by ProtobufContentSerializer()
21 |
22 | /**
23 | * Decodes a serialized object from a ByteReadPacket using ProtoBuf serialization.
24 | *
25 | * @param kType The KType representing the type of the object to be decoded.
26 | * @param packet The ByteReadPacket containing the serialized object data.
27 | * @return The deserialized object of type T.
28 | */
29 | @Suppress("UNCHECKED_CAST")
30 | @OptIn(ExperimentalSerializationApi::class)
31 | override fun decode(kType: KType, packet: ByteReadPacket): T {
32 | return protoBuf.decodeFromByteArray(
33 | (protoBuf.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
34 | packet.readBytes(),
35 | )
36 | }
37 |
38 | /**
39 | * Encodes the given value of type T to a ByteReadPacket using the ProtoBuf serialization.
40 | *
41 | * @param kType The Kotlin type of the value.
42 | * @param value The value to encode.
43 | * @return The encoded value as a ByteReadPacket.
44 | */
45 | @Suppress("UNCHECKED_CAST")
46 | override fun encode(kType: KType, value: T): ByteReadPacket {
47 | return ByteReadPacket(
48 | protoBuf.encodeToByteArray(
49 | (protoBuf.serializersModule.serializerOrNull(kType) ?: serializer()) as KSerializer,
50 | value,
51 | )
52 | )
53 | }
54 | }
--------------------------------------------------------------------------------
/router-serialization/test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | alias(libs.plugins.kotlinx.serialization)
4 | }
5 |
6 | dependencies {
7 | commonMainImplementation(libs.rsocket.server)
8 |
9 |
10 | commonMainImplementation(projects.routerCore)
11 | commonMainImplementation(projects.routerCore.test)
12 | commonMainImplementation(projects.routerSerialization.core)
13 |
14 | jvmTestImplementation(libs.kotlinx.serialization.json)
15 | jvmTestImplementation(libs.kotlinx.serialization.cbor)
16 | jvmTestImplementation(libs.kotlinx.serialization.protobuf)
17 |
18 | jvmTestImplementation(projects.routerSerialization.json)
19 | jvmTestImplementation(projects.routerSerialization.cbor)
20 | jvmTestImplementation(projects.routerSerialization.protobuf)
21 | jvmTestImplementation(libs.kotlin.test)
22 | }
23 |
24 | mavenPublishing {
25 | coordinates(
26 | groupId = "com.y9vad9.rsocket.router",
27 | artifactId = "router-serialization-test",
28 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
29 | )
30 |
31 | pom {
32 | name.set("Router Serialization Testing")
33 | description.set(
34 | """
35 | Kotlin RSocket library for testing type-safe serializable routes. Experimental: can be dropped or changed
36 | at any time.
37 | """.trimIndent()
38 | )
39 | }
40 | }
--------------------------------------------------------------------------------
/router-serialization/test/src/commonMain/kotlin/com/y9vad9/rsocket/router/serialization/test/RouterExt.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(
2 | ExperimentalInterceptorsApi::class, InternalRouterSerializationApi::class,
3 | InternalRouterSerializationApi::class,
4 | )
5 |
6 | package com.y9vad9.rsocket.router.serialization.test
7 |
8 | import com.y9vad9.rsocket.router.Route
9 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
10 | import com.y9vad9.rsocket.router.serialization.annotations.InternalRouterSerializationApi
11 | import com.y9vad9.rsocket.router.serialization.decode
12 | import com.y9vad9.rsocket.router.serialization.encode
13 | import com.y9vad9.rsocket.router.serialization.preprocessor.SerializationProvider
14 | import com.y9vad9.rsocket.router.test.fireAndForgetOrAssert
15 | import com.y9vad9.rsocket.router.test.requestChannelOrAssert
16 | import com.y9vad9.rsocket.router.test.requestResponseOrAssert
17 | import com.y9vad9.rsocket.router.test.requestStreamOrAssert
18 | import io.ktor.server.routing.*
19 | import io.ktor.utils.io.core.*
20 | import io.rsocket.kotlin.payload.Payload
21 | import kotlinx.coroutines.flow.Flow
22 | import kotlinx.coroutines.flow.map
23 |
24 | public suspend inline fun Route.requestResponseOrAssert(
25 | data: T,
26 | metadata: ByteReadPacket? = null,
27 | ): R {
28 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
29 |
30 | return requestResponseOrAssert(
31 | Payload(
32 | data = contentSerializer.encode(data),
33 | metadata = metadata,
34 | )
35 | ).let { contentSerializer.decode(it.data) }
36 | }
37 |
38 | public suspend inline fun Route.fireAndForgetOrAssert(
39 | data: T,
40 | metadata: ByteReadPacket? = null,
41 | ) {
42 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
43 |
44 | return fireAndForgetOrAssert(
45 | Payload(
46 | data = contentSerializer.encode(data),
47 | metadata = metadata,
48 | )
49 | )
50 | }
51 |
52 | public suspend inline fun Route.requestStreamOrAssert(
53 | data: T,
54 | metadata: ByteReadPacket? = null,
55 | ): Flow {
56 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
57 |
58 | return requestStreamOrAssert(
59 | Payload(
60 | data = contentSerializer.encode(data),
61 | metadata = metadata,
62 | )
63 | ).let { flow ->
64 | flow.map { contentSerializer.decode(it.data) }
65 | }
66 | }
67 |
68 | public suspend inline fun Route.requestChannelOrAssert(
69 | initial: T,
70 | payloads: Flow,
71 | metadata: ByteReadPacket? = null,
72 | ): Flow {
73 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
74 |
75 | return requestChannelOrAssert(
76 | initPayload = Payload(
77 | data = contentSerializer.encode(initial),
78 | metadata = metadata,
79 | ),
80 | payloads = payloads.map { Payload(contentSerializer.encode(it)) },
81 | ).let { flow ->
82 | flow.map { contentSerializer.decode(it.data) }
83 | }
84 | }
--------------------------------------------------------------------------------
/router-serialization/test/src/jvmTest/kotlin/com/y9vad9/rsocket/router/serialization/SerializableRouterTest.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.serialization
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.router
5 | import com.y9vad9.rsocket.router.serialization.annotations.ExperimentalRouterSerializationApi
6 | import com.y9vad9.rsocket.router.serialization.json.CborContentSerializer
7 | import com.y9vad9.rsocket.router.serialization.json.JsonContentSerializer
8 | import com.y9vad9.rsocket.router.serialization.json.ProtobufContentSerializer
9 | import com.y9vad9.rsocket.router.serialization.preprocessor.SerializationProvider
10 | import com.y9vad9.rsocket.router.serialization.preprocessor.serialization
11 | import com.y9vad9.rsocket.router.serialization.test.requestResponseOrAssert
12 | import com.y9vad9.rsocket.router.test.requestResponseOrAssert
13 | import com.y9vad9.rsocket.router.test.routeAtOrAssert
14 | import io.rsocket.kotlin.ExperimentalMetadataApi
15 | import io.rsocket.kotlin.metadata.RoutingMetadata
16 | import io.rsocket.kotlin.metadata.RoutingMetadata.Reader.read
17 | import io.rsocket.kotlin.metadata.read
18 | import kotlinx.coroutines.runBlocking
19 | import kotlinx.coroutines.withContext
20 | import kotlinx.serialization.ExperimentalSerializationApi
21 | import kotlinx.serialization.Serializable
22 | import kotlinx.serialization.json.Json
23 | import kotlin.test.Test
24 | import kotlin.test.assertEquals
25 |
26 | @OptIn(ExperimentalRouterSerializationApi::class)
27 | class SerializableRouterTest {
28 | @Serializable
29 | private data class Foo(val bar: Int)
30 |
31 | @Serializable
32 | private data class Bar(val foo: Int)
33 |
34 | private val router = router {
35 | serialization { _, _ -> JsonContentSerializer(Json) }
36 | routeProvider { _ -> TODO() }
37 |
38 | routing {
39 | route("test") {
40 | requestResponse {
41 | Bar(it.bar)
42 | }
43 | }
44 | }
45 | }
46 |
47 | @ExperimentalSerializationApi
48 | @OptIn(ExperimentalInterceptorsApi::class)
49 | @Test
50 | fun `check serialization`() {
51 | val serializers = listOf(
52 | JsonContentSerializer,
53 | CborContentSerializer,
54 | ProtobufContentSerializer
55 | )
56 |
57 | runBlocking {
58 | serializers.forEach { contentSerializer ->
59 | withContext(SerializationProvider.asContextElement(contentSerializer)) {
60 | val result = router.routeAtOrAssert("test")
61 | .requestResponseOrAssert(
62 | data = Foo(0),
63 | )
64 |
65 | assertEquals(result.foo, 0)
66 | }
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/router-versioning/README.md:
--------------------------------------------------------------------------------
1 | # Route Versioning
2 |
3 | When a product grows and evolves, dealing with backward and forward compatibility becomes essential. For these purposes, the library provides necessary wrappers around `router-core` with versioning support.
4 |
5 | > **Warning**
6 | > This feature is experimental; migration steps might be required in the future.
7 |
8 | ## How It Works
9 |
10 | Every type of request with the `router-versioning-core` artifact now has its extension with a DSL builder for versioning:
11 |
12 | ```kotlin
13 | val router = {
14 | routeProvider { /*...*/ }
15 | versioning { coroutineContext, payload -> Version(/* ... */) }
16 |
17 | routing {
18 | route("authorization") {
19 | requestResponseV("register") {
20 | // available from version 1 up to 2
21 | version(1) { payload ->
22 | TODO()
23 | }
24 |
25 | // available from version 2 and onwards
26 | version(2) { payload ->
27 | TODO()
28 | }
29 | }
30 | }
31 | }
32 | }
33 | ```
34 | > **Note**
35 | > As for semantic versioning, you can also specify minor version for each new request within one major release if
36 | > you need using `version(version: Version, block: suspend (T) -> R)`.
37 |
38 | ## Implementation
39 | To implement this feature, add it to your dependencies as follows:
40 | ```kotlin
41 | dependencies {
42 | implementation("com.y9vad9.rsocket.router:router-versioning-core:$version")
43 | }
44 | ```
45 |
46 | ## Serialization support
47 | To use [serialization feature](../router-serialization), implement the following dependency:
48 | ```kotlin
49 | dependencies {
50 | implementation("com.y9vad9.rsocket.router:router-versioning-serialization:$version")
51 | }
52 | ```
53 | ### Example
54 | Here's example of how you can define type-safe requests with versioning support:
55 | ```kotlin
56 | requestResponseV("register") {
57 | version(1) { foo: Foo ->
58 | Bar(/* ... */)
59 | }
60 | }
61 | ```
62 |
--------------------------------------------------------------------------------
/router-versioning/core/README.md:
--------------------------------------------------------------------------------
1 | # `router-versioned-core`
2 |
3 | This artifact is an auxiliary to the base RSocket Kotlin Router library. It enhances your RSocket
4 | services with routing layer by adding support for semantic versioning.
5 |
6 | ## Version Providers
7 |
8 | This artifact introduces the concept of Version Providers. This is encapsulated by
9 | the `VersionPreprocessor` class used to extract and process version details from the incoming RSocket payload.
10 | You must override this class to provide a custom method to fetch version details, typically fetched from the metadata of
11 | the payload.
12 |
13 | ## Routing and Versioning
14 |
15 | `router-versioned-core` provides an intuitive DSL for routing and versioning, simplifying the user interaction model.
16 | Let's take a brief look at how to use it:
17 |
18 | ```kotlin
19 | @OptIn(ExperimentalInterceptorsApi::class)
20 | public class ApiVersionProvider : RequestVersionProvider() {
21 | override fun version(payload: Payload): Version {
22 | TODO()
23 | }
24 | }
25 |
26 | public val Version.Companion.V1_0: Version by lazy { Version(1, 0) }
27 | public val Version.Companion.V2_0: Version by lazy { Version(2, 0) }
28 |
29 | val router: Router = router {
30 | preprocessors {
31 | forCoroutineContext(MyVersionPreprocessor())
32 | }
33 |
34 | routing {
35 | route("auth") {
36 | // short version
37 | requestResponseV("start") {
38 | version(1) { payload ->
39 | // handle requests for version "1.0"
40 | Payload.Empty
41 | }
42 | version(2) { payload ->
43 | // handle requests for version "2.0"
44 | Payload.Empty
45 | }
46 | }
47 | // or longer version
48 | requestStreamVersioned("confirm") {
49 | // you can specify version up to minor and patch
50 | version(Version.V1_0) { payload ->
51 | // handle requests for version "1.0"
52 | flow(Payload.Empty)
53 | }
54 |
55 | // you can specify version up to minor and patch
56 | version(Version.V2_0) { payload ->
57 | // handle requests for version "2.0"
58 | flow(Payload.Empty)
59 | }
60 | }
61 | }
62 | }
63 | }
64 | ```
65 | > **Note**
66 | > But, you shouldn't use patch version for versioning your requests. It's used only as annotation.
67 | >
68 | > `minor` should also be used only as an annotation and only for versioning new requests ideally. Changing
69 | > contract of specific request should always happen on new major version. Read more about [semantic versioning](https://semver.org/).
70 |
71 | In this example, the DSL allows developers to define different handlers for each available version of their endpoints.
72 | This way, clients running on different versions can coexist and interact with the service in a consistent fashion.
73 |
74 | ## Implementation
75 | ```kotlin
76 | repositories {
77 | maven("https://maven.y9vad9.com")
78 | }
79 |
80 | dependencies {
81 | implementation("com.y9vad9.rsocket.router:router-versioned-core:$version")
82 | }
83 | ```
84 |
--------------------------------------------------------------------------------
/router-versioning/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 | commonMainImplementation(projects.routerCore)
8 |
9 | jvmTestImplementation(libs.kotlin.test)
10 | jvmTestImplementation(projects.routerCore.test)
11 | }
12 |
13 | mavenPublishing {
14 | coordinates(
15 | groupId = "com.y9vad9.rsocket.router",
16 | artifactId = "router-versioning-core",
17 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
18 | )
19 |
20 | pom {
21 | name.set("Router Versioning")
22 | description.set(
23 | """
24 | Kotlin RSocket library for version-safe routing. Provides semantic versioning mechanism for ensuring
25 | backward and forward compatibility.
26 | """.trimIndent()
27 | )
28 | }
29 | }
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/DeclarableRoutingBuilderExt.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning
2 |
3 | import com.y9vad9.rsocket.router.builders.DeclarableRoutingBuilder
4 | import com.y9vad9.rsocket.router.versioning.annotations.VersioningDsl
5 | import com.y9vad9.rsocket.router.versioning.builders.VersioningBuilder
6 | import com.y9vad9.rsocket.router.versioning.preprocessor.RequestVersionProvider
7 | import io.rsocket.kotlin.payload.Payload
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | /**
11 | * Shortened variant of [requestResponseVersioned].
12 | */
13 | @VersioningDsl
14 | public fun DeclarableRoutingBuilder.requestResponseV(
15 | path: String,
16 | block: VersioningBuilder.() -> Unit,
17 | ): Unit = requestResponseVersioned(path, block)
18 |
19 | public fun DeclarableRoutingBuilder.requestResponseVersioned(
20 | path: String,
21 | block: VersioningBuilder.() -> Unit,
22 | ): Unit = route(path) {
23 | requestResponseVersioned(block)
24 | }
25 |
26 | /**
27 | * Creates a request-response endpoint with versioning support.
28 | *
29 | * @param path The URL path for the endpoint.
30 | * @param block A lambda function that configures the endpoint using a [VersioningBuilder].
31 | *
32 | * @throws IllegalStateException if the [RequestVersionProvider] is not registered.
33 | */
34 | @VersioningDsl
35 | public fun DeclarableRoutingBuilder.requestResponseVersioned(
36 | block: VersioningBuilder.() -> Unit,
37 | ) {
38 | val versionedRequest = VersioningBuilder().apply(block).build()
39 |
40 | requestResponse { payload ->
41 | versionedRequest.execute(payload)
42 | }
43 | }
44 |
45 | @VersioningDsl
46 | public fun DeclarableRoutingBuilder.requestResponseV(
47 | block: VersioningBuilder.() -> Unit,
48 | ): Unit = requestResponseVersioned(block)
49 |
50 | /**
51 | * Shortened variant of [requestStreamVersioned].
52 | */
53 | @VersioningDsl
54 | public fun DeclarableRoutingBuilder.requestStreamV(
55 | path: String,
56 | block: VersioningBuilder>.() -> Unit,
57 | ): Unit = requestStreamVersioned(path, block)
58 |
59 |
60 | public fun DeclarableRoutingBuilder.requestStreamVersioned(
61 | path: String,
62 | block: VersioningBuilder>.() -> Unit,
63 | ): Unit = route(path) {
64 | requestStreamVersioned(block)
65 | }
66 |
67 | /**
68 | * Creates a request-stream endpoint with versioning support.
69 | *
70 | * @param path The URL path for the endpoint.
71 | * @param block A lambda function that configures the endpoint using a [VersioningBuilder].
72 | *
73 | * @throws IllegalStateException if the [RequestVersionProvider] is not registered.
74 | */
75 | @VersioningDsl
76 | public fun DeclarableRoutingBuilder.requestStreamVersioned(
77 | block: VersioningBuilder>.() -> Unit,
78 | ) {
79 | val versionedRequest = VersioningBuilder>().apply(block).build()
80 |
81 | requestStream { payload ->
82 | versionedRequest.execute(payload)
83 | }
84 | }
85 |
86 | @VersioningDsl
87 | public fun DeclarableRoutingBuilder.requestStreamV(
88 | block: VersioningBuilder>.() -> Unit,
89 | ): Unit = requestStreamVersioned(block)
90 |
91 | /**
92 | * Shortened variant of [fireAndForgetVersioned].
93 | */
94 | @VersioningDsl
95 | public fun DeclarableRoutingBuilder.fireAndForgetV(
96 | path: String,
97 | block: VersioningBuilder.() -> Unit,
98 | ): Unit = fireAndForgetVersioned(path, block)
99 |
100 |
101 | public fun DeclarableRoutingBuilder.fireAndForgetVersioned(
102 | path: String,
103 | block: VersioningBuilder.() -> Unit,
104 | ): Unit = route(path) {
105 | fireAndForgetVersioned(block)
106 | }
107 |
108 | /**
109 | * Creates a fireAndForget endpoint with versioning support.
110 | *
111 | * @param path The URL path for the endpoint.
112 | * @param block A lambda function that configures the endpoint using a [VersioningBuilder].
113 | *
114 | * @throws IllegalStateException if the [RequestVersionProvider] is not registered.
115 | */
116 | @VersioningDsl
117 | public fun DeclarableRoutingBuilder.fireAndForgetVersioned(
118 | block: VersioningBuilder.() -> Unit,
119 | ) {
120 | val versionedRequest = VersioningBuilder().apply(block).build()
121 |
122 | fireAndForget { payload ->
123 | versionedRequest.execute(payload)
124 | }
125 | }
126 |
127 | @VersioningDsl
128 | public fun DeclarableRoutingBuilder.fireAndForgetV(
129 | block: VersioningBuilder.() -> Unit,
130 | ): Unit = fireAndForgetVersioned(block)
131 |
132 |
133 | /**
134 | * Shortened variant of [requestChannelVersioned].
135 | */
136 | @VersioningDsl
137 | public fun DeclarableRoutingBuilder.requestChannelV(
138 | path: String,
139 | block: VersioningBuilder>.() -> Unit,
140 | ): Unit = requestChannelVersioned(path, block)
141 |
142 | public fun DeclarableRoutingBuilder.requestChannelVersioned(
143 | path: String,
144 | block: VersioningBuilder>.() -> Unit,
145 | ): Unit = route(path) {
146 | requestChannelVersioned(block)
147 | }
148 |
149 | /**
150 | * Creates a request-channel endpoint with versioning support.
151 | *
152 | * @param path The URL path for the endpoint.
153 | * @param block A lambda function that configures the endpoint using a [VersioningBuilder].
154 | *
155 | * @throws IllegalStateException if the [RequestVersionProvider] is not registered.
156 | */
157 | @VersioningDsl
158 | public fun DeclarableRoutingBuilder.requestChannelVersioned(
159 | block: VersioningBuilder>.() -> Unit,
160 | ) {
161 | val versionedRequest = VersioningBuilder>().apply(block).build()
162 |
163 | requestChannel { initPayload, payloads ->
164 | versionedRequest.execute(PayloadStream(initPayload, payloads))
165 | }
166 | }
167 |
168 | @VersioningDsl
169 | public fun DeclarableRoutingBuilder.requestChannelV(
170 | block: VersioningBuilder>.() -> Unit,
171 | ): Unit = requestChannelVersioned(block)
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/RouterBuilderExt.kt:
--------------------------------------------------------------------------------
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.RequestVersionProvider
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 : RequestVersionProvider() {
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:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.versioning.preprocessor.RequestVersionProvider
5 | import kotlin.coroutines.coroutineContext
6 |
7 | /**
8 | * Represents a version with major, minor, and patch components.
9 | *
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.
15 | */
16 | public data class Version(public val major: Int, public val minor: Int, public val patch: Int = 0) : Comparable {
17 | init {
18 | require(major >= 0) { "Major version cannot be negative" }
19 | require(minor >= 0) { "Minor version cannot be negative" }
20 | require(patch >= 0) { "Patch version cannot be negative" }
21 | }
22 |
23 | public companion object {
24 | /**
25 | * Represents the first version. In meaning of versioning, it means that
26 | * we accept request / route from any version.
27 | */
28 | public val ZERO: Version = Version(0, 0, 0)
29 | /**
30 | * Represents the latest version.
31 | *
32 | * In meaning of versioning, it means that request has no max version
33 | * and it's actual.
34 | *
35 | * @see Version
36 | */
37 | public val INDEFINITE: Version = Version(Int.MAX_VALUE, Int.MAX_VALUE, Int.MAX_VALUE)
38 | }
39 |
40 | /**
41 | * Compares this version with the specified version.
42 | *
43 | * @param other the version to be compared
44 | * @return a negative integer, zero, or a positive integer if this version is less than, equal to, or greater than
45 | * the specified version
46 | */
47 | override fun compareTo(other: Version): Int {
48 | return when {
49 | this.major != other.major -> this.major - other.major
50 | this.minor != other.minor -> this.minor - other.minor
51 | else -> this.patch - other.patch
52 | }
53 | }
54 | }
55 |
56 | /**
57 | * Creates a closed range of versions starting from this version and ending at another version.
58 | *
59 | * @param another The ending version of the range.
60 | * @return A closed range of versions from this version to the specified ending version.
61 | * @throws IllegalArgumentException If the ending version is negative.
62 | */
63 | public infix fun Version.until(another: Version): ClosedRange {
64 | return when {
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 | )
72 | else -> error("Unable to create `until` range – version cannot be negative.")
73 | }
74 | }
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/VersionRequirements.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning
2 |
3 | /**
4 | * Class representing the requirements for a version range.
5 | *
6 | * @property firstAcceptableVersion The first acceptable version in the range.
7 | * @property lastAcceptableVersion The last acceptable version in the range.
8 | */
9 | public data class VersionRequirements(
10 | val firstAcceptableVersion: Version,
11 | val lastAcceptableVersion: Version,
12 | ) {
13 | /**
14 | * Checks if the given version satisfies the acceptable version range.
15 | *
16 | * @param version The version to be checked against the acceptable version range.
17 | * @return true if the given version is within the acceptable range, false otherwise.
18 | */
19 | public fun satisfies(version: Version): Boolean =
20 | version in firstAcceptableVersion until lastAcceptableVersion
21 | }
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/VersionedRequest.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.versioning.annotations.InternalVersioningApi
5 | import com.y9vad9.rsocket.router.versioning.preprocessor.RequestVersionProvider
6 | import io.rsocket.kotlin.RSocketError
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | /**
10 | * A sealed class representing versioned requests.
11 | * @param T The input type of the request.
12 | * @param R The result type of the request.
13 | */
14 | internal sealed class VersionedRequest {
15 | /**
16 | * Executes the given input and returns the result.
17 | *
18 | * @param input The input to be executed.
19 | * @return The result of executing the input.
20 | */
21 | abstract suspend fun execute(input: T): R
22 |
23 | /**
24 | * Executes a single conditional request that checks if the input version satisfies the version requirements.
25 | *
26 | * @param T the type of the input parameter.
27 | * @param R the type of the return value.
28 | * @property function the suspend function to be executed.
29 | * @property versionRequirements the version requirements that need to be satisfied.
30 | */
31 | class SingleConditional(
32 | val function: suspend (T) -> R,
33 | val versionRequirements: VersionRequirements
34 | ) : VersionedRequest() {
35 | @OptIn(ExperimentalInterceptorsApi::class, InternalVersioningApi::class)
36 | override suspend fun execute(input: T): R {
37 | val version = RequestVersionProvider.getFromCoroutineContext()
38 |
39 | if (!versionRequirements.satisfies(version))
40 | throw RSocketError.Rejected("Request is not available for your API version.")
41 | return function(input)
42 | }
43 | }
44 |
45 | /**
46 | * A class that provides multiple conditional execution based on API version requirements.
47 | *
48 | * @param T The input type of the request.
49 | * @param R The result type of the request.
50 | * @property variants A list of pairs that associate version requirements with suspended functions that take input of type T and return output of type R.
51 | */
52 | data class MultipleConditional(
53 | val variants: List R>>
54 | ) : VersionedRequest() {
55 | @OptIn(ExperimentalInterceptorsApi::class, InternalVersioningApi::class)
56 | override suspend fun execute(input: T): R {
57 | val version = RequestVersionProvider.getFromCoroutineContext()
58 |
59 | return variants.firstOrNull { (requirement, _) ->
60 | requirement.satisfies(version)
61 | }?.also { println(it.first) }?.second?.invoke(input) ?: throw RSocketError.Rejected("Request is not available for your API version.")
62 | }
63 | }
64 | }
65 |
66 |
67 | public data class PayloadStream(
68 | val initPayload: io.rsocket.kotlin.payload.Payload,
69 | val payloads: Flow,
70 | )
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/annotations/ExperimentalVersioningApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning.annotations
2 |
3 | @RequiresOptIn(message = "Experimental API. Might be removed.", level = RequiresOptIn.Level.ERROR)
4 | public annotation class ExperimentalVersioningApi
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/annotations/InternalVersioningApi.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning.annotations
2 |
3 | @RequiresOptIn(message = "Internal API. Use with extra considerations.", level = RequiresOptIn.Level.ERROR)
4 | public annotation class InternalVersioningApi
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/annotations/VersioningDsl.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning.annotations
2 |
3 | @DslMarker
4 | public annotation class VersioningDsl
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/builders/VersioningBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning.builders
2 |
3 | import com.y9vad9.rsocket.router.versioning.Version
4 | import com.y9vad9.rsocket.router.versioning.VersionRequirements
5 | import com.y9vad9.rsocket.router.versioning.VersionedRequest
6 | import com.y9vad9.rsocket.router.versioning.until
7 |
8 | /**
9 | * Builder class used for versioning requests.
10 | * @param T The type of the request.
11 | * @param R The type of the response.
12 | */
13 | public class VersioningBuilder internal constructor() {
14 | private var versionedRequest: VersionedRequest? = null
15 |
16 | /**
17 | * Updates the versioned request based on the given version.
18 | *
19 | * @param from From which version to be applied.
20 | * @param until Until which version should be applied.
21 | * @param block The coroutine block that will be executed for the given version.
22 | */
23 | public fun version(
24 | from: Version,
25 | until: Version = Version.INDEFINITE,
26 | block: suspend (T) -> R,
27 | ) {
28 | versionedRequest = when (val versionedRequest = versionedRequest) {
29 | null -> {
30 | VersionedRequest.SingleConditional(
31 | function = block,
32 | VersionRequirements(firstAcceptableVersion = from, lastAcceptableVersion = until)
33 | )
34 | }
35 |
36 | is VersionedRequest.SingleConditional -> {
37 | VersionedRequest.MultipleConditional(
38 | variants = listOf(
39 | versionedRequest.versionRequirements.copy(
40 | lastAcceptableVersion = (versionedRequest.versionRequirements.firstAcceptableVersion until from).endInclusive
41 | ) to versionedRequest.function,
42 | VersionRequirements(from, Version.INDEFINITE) to block,
43 | )
44 | )
45 | }
46 |
47 | is VersionedRequest.MultipleConditional -> {
48 | versionedRequest.copy(
49 | variants = (versionedRequest.variants.mapIndexed { index, (requirements, function) ->
50 | if (index == versionedRequest.variants.lastIndex)
51 | requirements.copy(
52 | lastAcceptableVersion = (requirements.firstAcceptableVersion until from).endInclusive,
53 | ) to function
54 | else requirements to function
55 | } + (VersionRequirements(from, until) to block)).sortedBy {
56 | it.first.firstAcceptableVersion
57 | }
58 | )
59 | }
60 | }
61 | }
62 |
63 | internal fun build(): VersionedRequest = versionedRequest ?: error("No version was specified.")
64 | }
65 |
66 | /**
67 | * Adds a version to the VersioningBuilder.
68 | *
69 | * @param fromMajor The major version number from which variant will be applied.
70 | * @param untilMajor The major version number until which variant will be applied.
71 | * @param block The block of code to be executed for the given version.
72 | */
73 | public fun VersioningBuilder.version(
74 | fromMajor: Int,
75 | untilMajor: Int = Int.MAX_VALUE,
76 | block: suspend (T) -> R,
77 | ) {
78 | version(
79 | from = Version(major = fromMajor, minor = 0),
80 | until = Version(major = untilMajor, minor = 0),
81 | block = block,
82 | )
83 | }
--------------------------------------------------------------------------------
/router-versioning/core/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/preprocessor/RequestVersionProvider.kt:
--------------------------------------------------------------------------------
1 | package com.y9vad9.rsocket.router.versioning.preprocessor
2 |
3 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
4 | import com.y9vad9.rsocket.router.interceptors.Preprocessor
5 | import com.y9vad9.rsocket.router.versioning.Version
6 | import com.y9vad9.rsocket.router.versioning.annotations.ExperimentalVersioningApi
7 | import com.y9vad9.rsocket.router.versioning.annotations.InternalVersioningApi
8 | import io.rsocket.kotlin.payload.Payload
9 | import kotlin.coroutines.CoroutineContext
10 | import kotlin.coroutines.coroutineContext
11 |
12 | @ExperimentalInterceptorsApi
13 | public abstract class RequestVersionProvider : Preprocessor.CoroutineContext {
14 | internal data class VersionElement(val version: Version) : CoroutineContext.Element {
15 | companion object Key : CoroutineContext.Key
16 |
17 | override val key: CoroutineContext.Key<*>
18 | get() = Key
19 | }
20 |
21 | public companion object {
22 | /**
23 | * Retrieves the version from the coroutine context.
24 | *
25 | * **API Note**:
26 | * You shouldn't call this function yourself unless you use it to define your own extensions
27 | * that should be dependent on it.
28 | *
29 | * **Failure note**:
30 | * 1) if you didn't call it yourself, probably you need to register `RequestVersionProvider` by
31 | * putting it in the preprocessors or by using `versioning` function in `RoutingBuilder`.
32 | * 2) if function wasn't called by you intentionally and `RequestVersionProvider` is already
33 | * registered, but inside `test` you should provide content serializer to context using
34 | * [com.y9vad9.rsocket.router.test.preprocess] or [com.y9vad9.rsocket.router.intercepts] functions.
35 | *
36 | * @return The version extracted from the coroutine context.
37 | * @throws IllegalStateException if the RequestVersionProvider was not registered or called from an illegal context.
38 | */
39 | @InternalVersioningApi
40 | public suspend fun getFromCoroutineContext(): Version {
41 | return coroutineContext[VersionElement]?.version
42 | ?: error("RequestVersionProvider was not registered or call happened from illegal context.")
43 | }
44 |
45 | /**
46 | * Retrieves the version from the payload and adds it as a context element to the coroutine context.
47 | *
48 | * **Note**: You should use this function inside tests to provide version.
49 | *
50 | * @param version The version extracted from the payload.
51 | * @return The updated coroutine context with the version as a context element.
52 | */
53 | @ExperimentalVersioningApi
54 | @InternalVersioningApi
55 | public suspend fun asContextElement(version: Version): CoroutineContext =
56 | coroutineContext + VersionElement(version)
57 | }
58 |
59 | /**
60 | * Retrieves the version from the specified payload.
61 | *
62 | * @param payload The payload containing the version information.
63 | * @return The version extracted from the payload.
64 | */
65 | public abstract fun version(payload: Payload, coroutineContext: CoroutineContext): Version
66 |
67 | final override fun intercept(coroutineContext: CoroutineContext, input: Payload): CoroutineContext {
68 | return coroutineContext + VersionElement(version(input, coroutineContext))
69 | }
70 | }
--------------------------------------------------------------------------------
/router-versioning/core/src/jvmTest/kotlin/com/y9vad9/rsocket/router/versioning/test/DeclarableRoutingBuilderExtTest.kt:
--------------------------------------------------------------------------------
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.RequestVersionProvider
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 = RequestVersionProvider.VersionElement(Version(1, 0))
49 |
50 | // THEN
51 | assertFailsWith {
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 | RequestVersionProvider.VersionElement(Version(2, 0)),
64 | RequestVersionProvider.VersionElement(Version(2, 1)),
65 | RequestVersionProvider.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 | RequestVersionProvider.VersionElement(Version(3, 0)),
88 | RequestVersionProvider.VersionElement(Version(3, 9)),
89 | RequestVersionProvider.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 | }
--------------------------------------------------------------------------------
/router-versioning/serialization/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(libs.plugins.multiplatform.module.convention.get().pluginId)
3 | }
4 |
5 | dependencies {
6 | commonMainImplementation(libs.rsocket.server)
7 | commonMainImplementation(libs.kotlinx.serialization.core)
8 |
9 | commonMainImplementation(projects.routerVersioning.core)
10 | commonMainImplementation(projects.routerSerialization.core)
11 |
12 | commonMainImplementation(projects.routerCore)
13 | }
14 |
15 | mavenPublishing {
16 | coordinates(
17 | groupId = "com.y9vad9.rsocket.router",
18 | artifactId = "router-versioning-serialization",
19 | version = System.getenv("LIB_VERSION") ?: return@mavenPublishing
20 | )
21 |
22 | pom {
23 | name.set("Router Versioning Serialization Adapter")
24 | description.set("""
25 | Kotlin RSocket library for supporting serialization mechanism in versioned routes.
26 | """.trimIndent()
27 | )
28 | }
29 | }
--------------------------------------------------------------------------------
/router-versioning/serialization/src/commonMain/kotlin/com/y9vad9/rsocket/router/versioning/serialization/DeclarableRoutingBuilderExt.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalInterceptorsApi::class, InternalRouterSerializationApi::class)
2 |
3 | package com.y9vad9.rsocket.router.versioning.serialization
4 |
5 | import com.y9vad9.rsocket.router.annotations.ExperimentalInterceptorsApi
6 | import com.y9vad9.rsocket.router.serialization.annotations.InternalRouterSerializationApi
7 | import com.y9vad9.rsocket.router.serialization.decode
8 | import com.y9vad9.rsocket.router.serialization.encode
9 | import com.y9vad9.rsocket.router.serialization.preprocessor.SerializationProvider
10 | import com.y9vad9.rsocket.router.versioning.PayloadStream
11 | import com.y9vad9.rsocket.router.versioning.Version
12 | import com.y9vad9.rsocket.router.versioning.builders.VersioningBuilder
13 | import io.rsocket.kotlin.payload.Payload
14 | import kotlinx.coroutines.flow.Flow
15 | import kotlinx.coroutines.flow.map
16 |
17 | @JvmName("versionRequestResponse")
18 | public inline fun VersioningBuilder.version(
19 | from: Version,
20 | until: Version = Version.INDEFINITE,
21 | crossinline block: suspend (T) -> R,
22 | ) {
23 | version(from, until) { payload ->
24 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
25 |
26 | block(contentSerializer.decode(payload.data))
27 | .let { contentSerializer.encode(it) }
28 | .let { Payload(it) }
29 | }
30 | }
31 |
32 | @JvmName("versionFireAndForget")
33 | public inline fun VersioningBuilder.version(
34 | from: Version,
35 | until: Version = Version.INDEFINITE,
36 | crossinline block: suspend (T) -> Unit,
37 | ) {
38 | version(from, until) { payload ->
39 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
40 |
41 | block(contentSerializer.decode(payload.data))
42 | }
43 | }
44 |
45 | @JvmName("versionRequestStream")
46 | public inline fun VersioningBuilder>.version(
47 | from: Version,
48 | until: Version = Version.INDEFINITE,
49 | crossinline block: suspend (T) -> Flow,
50 | ) {
51 | version(from, until) { payload ->
52 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
53 |
54 | block(contentSerializer.decode(payload.data))
55 | .map { Payload(contentSerializer.encode(it)) }
56 | }
57 | }
58 |
59 | @JvmName("versionRequestChannel")
60 | @OptIn(InternalRouterSerializationApi::class)
61 | public inline fun VersioningBuilder>.version(
62 | from: Version,
63 | until: Version = Version.INDEFINITE,
64 | crossinline block: suspend (initial: T, payloads: Flow) -> Flow,
65 | ) {
66 | version(from, until) { payload ->
67 | val contentSerializer = SerializationProvider.getFromCoroutineContext()
68 |
69 | val initial: T = contentSerializer.decode(payload.initPayload.data)
70 | val payloads: Flow = payload.payloads.map { contentSerializer.decode(it.data) }
71 |
72 | block(initial, payloads)
73 | .map { Payload(contentSerializer.encode(it)) }
74 | }
75 | }
76 |
77 | @JvmName("versionRequestResponse")
78 | public inline fun VersioningBuilder.version(
79 | fromMajor: Int,
80 | untilMajor: Int = Int.MAX_VALUE,
81 | crossinline block: suspend (T) -> R,
82 | ) {
83 | version(
84 | from = Version(major = fromMajor, minor = 0),
85 | until = Version(major = untilMajor, minor = 0),
86 | block = block,
87 | )
88 | }
89 |
90 | @JvmName("versionFireAndForget")
91 | public inline fun VersioningBuilder.version(
92 | fromMajor: Int,
93 | untilMajor: Int = Int.MAX_VALUE,
94 | crossinline block: suspend (T) -> Unit,
95 | ) {
96 | version(
97 | from = Version(major = fromMajor, minor = 0),
98 | until = Version(major = untilMajor, minor = 0),
99 | block = block,
100 | )
101 | }
102 |
103 | @JvmName("versionRequestStream")
104 | public inline fun VersioningBuilder>.version(
105 | fromMajor: Int,
106 | untilMajor: Int = Int.MAX_VALUE,
107 | crossinline block: suspend (T) -> Flow,
108 | ) {
109 | version(
110 | from = Version(major = fromMajor, minor = 0),
111 | until = Version(major = untilMajor, minor = 0),
112 | block = block,
113 | )
114 | }
115 |
116 |
117 | @JvmName("versionRequestChannel")
118 | public inline fun VersioningBuilder>.version(
119 | fromMajor: Int,
120 | untilMajor: Int = Int.MAX_VALUE,
121 | crossinline block: suspend (initial: T, payloads: Flow) -> Flow,
122 | ) {
123 | version(
124 | from = Version(major = fromMajor, minor = 0),
125 | until = Version(major = untilMajor, minor = 0),
126 | block = block,
127 | )
128 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2 |
3 | dependencyResolutionManagement {
4 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
5 | repositories {
6 | google()
7 | mavenCentral()
8 | maven("https://jitpack.io")
9 | maven("https://maven.y9vad9.com")
10 | }
11 | }
12 |
13 | pluginManagement {
14 | repositories {
15 | gradlePluginPortal()
16 | mavenCentral()
17 | google()
18 | }
19 | }
20 |
21 | rootProject.name = "rsocket-kotlin-router"
22 |
23 | includeBuild("build-conventions")
24 |
25 | include(":router-core", ":router-core:test")
26 | include(
27 | ":router-versioning:core",
28 | ":router-versioning:serialization",
29 | )
30 | include(
31 | ":router-serialization:core",
32 | ":router-serialization:json",
33 | ":router-serialization:protobuf",
34 | ":router-serialization:cbor",
35 | ":router-serialization:test",
36 | )
--------------------------------------------------------------------------------