├── .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 | ![GitHub release](https://img.shields.io/github/v/release/y9vad9/rsocket-kotlin-router) ![GitHub](https://img.shields.io/github/license/y9vad9/rsocket-kotlin-router) 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 | ) --------------------------------------------------------------------------------