Skip to content

Commit

Permalink
Refactors Ktor example to include more frontend targets
Browse files Browse the repository at this point in the history
franciscoengenheiro committed Mar 17, 2024
1 parent 7f8a36d commit 121a757
Showing 32 changed files with 192 additions and 130 deletions.
113 changes: 76 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
# Experiments

## KMP - Kotlin Multiplatform
## Table of Contents

- [Kotlin Multiplatform](#kotlin-multiplatform)
- [Architecture Overview](#architecture-overview)
- [Testing the Application](#testing-the-application)
- [Intermediate Source Sets](#intermediate-source-sets)
- [Adapter](#adapter)
- [Relevant Design Choices](#relevant-design-choices)
- [Kotlin-Js Interop](#kotlin-js-interop)
- [Javascript to Kotlin](#javascript-to-kotlin)
- [Demonstrations](#demonstrations)
- [NPM Dependencies](#npm-dependencies)
- [Build and Run](#build-and-run)
- [Kotlin to Javascript](#kotlin-to-javascript)
- [Demonstration](#demonstration)
- [Run](#run)
- [References](#references)
- [Ktor Framework](#ktor-framework)
- [Server](#server)
- [Launching the Application](#launching-the-application)
- [Define Application Module](#define-application-module)
- [Installing Plugins](#installing-plugins)
- [Defining Routes](#defining-routes)
- [Testing the Application](#testing-the-application-1)
- [Client](#client)
- [Requests](#requests)
- [Responses](#responses)
- [Demonstration](#demonstration-1)

## Kotlin Multiplatform

> The [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) (KMP) technology facilitates the sharing of
> application code across several platforms,
@@ -24,10 +53,10 @@ following situations:

1. A certain functionality cannot be implemented commonly because:

- It requires access to specific target APIs;
- The libraries available for common code _(i.e., Standard Kotlin Library, Kotlinx)_ do not cover the desired
functionalities,
and there's no external KMP-compatible library available to be used as a dependency (or it is discouraged to use);
- It requires access to specific target APIs;
- The libraries available for common code _(i.e., Standard Kotlin Library, Kotlinx)_ do not cover the desired
functionalities,
and there's no external KMP-compatible library available to be used as a dependency (or it is discouraged to use);

2. A certain target does not directly support KMP _(e.g., Node.js)_, and thus an [adapter](#adapter) is needed for the
code to be callable from the target.
@@ -48,9 +77,9 @@ actual fun platformName(): String = "JVM"
actual fun platformName(): String = "JS"
```

| ![KMP Architecture](./docs/imgs/kmp-architecture.png) |
|:-----------------------------------------------------:|
| KMP Architecture Overview |
| <img src="./docs/imgs/kmp-architecture.png" alt="KMP Architecture" width="50%"> |
|:-------------------------------------------------------------------------------:|
| KMP Architecture Overview |

### Testing the Application

@@ -60,12 +89,14 @@ and replaced by a few examples to practice the `expect/actual` mechanism more th

This [addition](./kmp/src/commonMain/kotlin) follows the same principles:

- test common functionality in [CommonTest](./kmp/src/commonTest/kotlin);
- test platform-specific functionality in each platform's test source set (`<Platform>Test`)
- **test common functionality** in [CommonTest](./kmp/src/commonTest/kotlin);
- **test platform-specific functionality** in each platform's test source set (`<Platform>Test`)

> To run the tests for all supported targets, use the command:
>
> `./gradlew kmp:allTests`
To run the tests for all supported targets, use the command:

```bash
./gradlew kmp:allTests
```

> [!IMPORTANT]
> There's currently a Native target's dependency issue,
@@ -110,7 +141,7 @@ essentially acting as a consumer.
```bash
# from root
node js-app/src/main/js/server.mjs
# take a look at the express paths and PORT configured in `server.mjs`
# take a look at the express paths and PORT configured in the server
# open an HTTP client and access http://localhost:PORT
```

@@ -173,8 +204,8 @@ the dependencies must be added to the `dependencies` block of the `build.gradle.

```kotlin
dependencies {
// Install npm dependencies
implementation(npm("randomstring", "1.3.0"))
// Install npm dependencies
implementation(npm("randomstring", "1.3.0"))
}
```

@@ -204,9 +235,9 @@ randomstring.generate(7);
@JsModule("randomstring")
@JsNonModule
external object RandomStringFromNpm {
fun generate(
length: Int = definedExternally,
): String
fun generate(
length: Int = definedExternally,
): String
}
```

@@ -251,7 +282,7 @@ external object RandomStringFromNpm {
node kotlin-js-interop/src/main/js/importing.mjs
```

#### References
### References

- [Kotlinlang: JS to Kotlin Interop](https://kotlinlang.org/docs/js-to-kotlin-interop.html)
- [Kotlinlang: Kotlin to JS Interop](https://kotlinlang.org/docs/js-interop.html)
@@ -261,9 +292,19 @@ node kotlin-js-interop/src/main/js/importing.mjs

## Ktor Framework

> [Ktor](https://ktor.io) is a modular framework for developing asynchronous server and client applications.
>
> Developed by _JetBrains_, it was built with pure _Kotlin_ and is integrated with
> the [Coroutines](https://github.com/Kotlin/kotlinx.coroutines)
> system. This system allows asynchronous code to be defined sequentially
> and can be executed without blocking threads, taking greater advantage of the computing system
> available
Module: [ktor](./ktor)

### Launching the Application
### Server

#### Launching the Application

The application can be launched using the `Application` class.

@@ -275,19 +316,20 @@ fun main() {
}
```

### Define Application Module
#### Define Application Module

In _Ktor_, the application module is using the `Application` class.
In _Ktor_, the application module is defined using the `Application` class.

```kotlin
fun Application.module() {
// ...
}
```

### Installing Plugins
#### Installing Plugins

Each plugin has its own configuration, which can be set using the `install` function.
A plugin can be installed using the `install` function and configured using its **last parameter function** (_trailing
lambda_).

```kotlin
fun Application.module() {
@@ -313,7 +355,7 @@ implementation("io.ktor:ktor-server-call-logging")
implementation("io.ktor:ktor-server-websockets")
```

### Defining Routes
#### Defining Routes

Routes can be defined using the `routing` function.

@@ -331,41 +373,34 @@ fun Application.module() {
}
```

### Testing the Application
#### Testing the Application

To test the application, the `testApplication` function can be used which exposes a `client` object that
can be used to perform requests to the server.
To test the application, you can utilize the `testApplication` function, which provides access to a `client` object for
making requests to the server.

Example:

```kotlin
testApplication {
val log = arrayListOf<String>()

// We perform a test websocket connection to this route. Effectively acting as a client.
// The [incoming] parameter allows receiving frames, while the [outgoing] allows sending frames to the server.
val client = client.config {
install(WebSockets)
}

client.webSocket("/ws") {
// Send a HELLO message
outgoing.send(Frame.Text("HELLO"))

// We then receive two messages (the message notifying that the member joined, and the message we sent echoed to us)
for (n in 0 until 2) {
log += (incoming.receive() as Frame.Text).readText()
}
}

// asserts
assertEquals(listOf("Member joined", "HELLO"), log)
}
```

### Client

Similar to the `Application` class,
Similar to the `Application` class as seen in the [Server](#server) section,
the `HttpClient` class can be used to perform requests to a server and install plugins.

```kotlin
@@ -405,3 +440,7 @@ val stringBody: String = httpResponse.body()
```

More examples [at](https://ktor.io/docs/response.html).

### Demonstration

TODO
6 changes: 4 additions & 2 deletions convention-plugins/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ dependencies {
implementation(libs.nexus.publish)
}

kotlin {
jvmToolchain(17)
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ plugins {
}

allprojects {
group = "kresi.experiments"
group = "kresil-experiments"
version = "0.0.1"
}

2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
25 changes: 25 additions & 0 deletions kmp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -5,6 +5,11 @@ plugins {
kotlin("plugin.serialization") version "1.9.22"
}

repositories {
mavenCentral()
google()
}

kotlin {
applyDefaultHierarchyTemplate()
jvm()
@@ -104,4 +109,24 @@ android {
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
/*sourceSets.all {
java.srcDirs(file("src/android${name.capitalize()}/kotlin"))
res.srcDirs(file("src/android${name.capitalize()}/res"))
resources.srcDirs(file("src/android${name.capitalize()}/resources"))
manifest.srcFile(file("src/android${name.capitalize()}/AndroidManifest.xml"))
}*/
sourceSets {
getByName("main") {
java.srcDirs("src/androidMain/kotlin")
res.srcDirs("src/androidMain/res")
}
getByName("test") {
java.srcDirs("src/androidTest/kotlin")
res.srcDirs("src/androidTest/res")
}
}
}
4 changes: 4 additions & 0 deletions kmp/src/androidMain/kotlin/models/Book.android.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package models

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual data class Book actual constructor(val title: String, val author: String)
14 changes: 0 additions & 14 deletions kmp/src/androidUnitTest/kotlin/AndroidPlatformTest.kt

This file was deleted.

2 changes: 1 addition & 1 deletion kmp/src/commonMain/kotlin/models/Book.kt
Original file line number Diff line number Diff line change
@@ -4,5 +4,5 @@ package models
* A simple book model.
* Needs to be marked expect because of js export
*/
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "NO_ACTUAL_FOR_EXPECT")
expect class Book(title: String, author: String)
42 changes: 0 additions & 42 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
@@ -390,13 +390,6 @@ abab@^2.0.6:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==

abort-controller@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"

accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@@ -1008,11 +1001,6 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==

event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==

eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@@ -1856,13 +1844,6 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==

node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"

node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
@@ -2561,11 +2542,6 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==

tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==

type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -2654,11 +2630,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"

webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==

webpack-cli@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891"
@@ -2790,14 +2761,6 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==

whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"

which@^1.2.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@@ -2836,11 +2799,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==

ws@8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==

ws@^8.13.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
99 changes: 68 additions & 31 deletions ktor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
import org.jetbrains.kotlin.gradle.DeprecatedTargetPresetApi
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21")
}
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}

repositories {
mavenCentral()
}

plugins {
id("kotlin-multiplatform")
google()
}

kotlin {
@OptIn(DeprecatedTargetPresetApi::class, InternalKotlinGradlePluginApi::class)
targets {
js("frontend", IR) {
browser {
testTask { enabled = false }

@OptIn(ExperimentalDistributionDsl::class)
distribution {
directory = file("$projectDir/src/backendMain/resources/web")
}
binaries.executable()
applyDefaultHierarchyTemplate()
jvm("backendJvm") // this creates both Main and Test source sets
androidTarget("frontendAndroid") {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js("frontendJs", IR) {
browser {
testTask { enabled = false }
@OptIn(ExperimentalDistributionDsl::class)
distribution {
directory = file("$projectDir/src/backendJVMMain/resources/web")
}
binaries.executable()
}
jvm("backend")
}

sourceSets.forEach {
@@ -43,9 +38,23 @@ kotlin {
}

sourceSets {
val backendMain by getting {

val commonMain by getting {
dependencies {

}
}

val commonTest by getting {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-test")
}
}

val backendJvmMain by getting {
dependsOn(commonMain)
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22")
implementation("io.ktor:ktor-server-netty")
implementation("io.ktor:ktor-server-websockets")
implementation("io.ktor:ktor-server-call-logging")
@@ -56,21 +65,49 @@ kotlin {
}
}

val backendTest by getting {
val backendJvmTest by getting {
dependsOn(commonTest)
dependencies {
implementation("io.ktor:ktor-server-test-host")
implementation("io.ktor:ktor-client-websockets")
implementation("org.jetbrains.kotlin:kotlin-test")
}
}

val frontendMain by getting {
val frontendMain by creating {
dependsOn(commonMain)
dependencies {

}
}

val frontendJsMain by getting {
dependsOn(frontendMain)
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-js")
implementation("io.ktor:ktor-client-websockets")
implementation("org.jetbrains.kotlin:kotlin-stdlib-js")
implementation("io.ktor:ktor-client-js")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.6.4")
}
}

val frontendAndroidMain by getting {
dependsOn(frontendMain)
dependencies {

}
}
}
}

android {
compileSdk = 32
// sourceSets["main"].manifest.srcFile("src/frontendAndroidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
namespace = "kresil-experiments"
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

package plugins

import io.ktor.server.application.*
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import connection.WsClient
import connection.dom.ChatDOMHandler
import connection.initConnection
import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import kotlinx.coroutines.DelicateCoroutinesApi
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import ChatDOMHandler.appendMessage
package connection

import connection.dom.ChatDOMHandler.appendMessage
import io.ktor.client.plugins.websocket.*
import kotlinx.browser.window
import kotlinx.coroutines.DelicateCoroutinesApi
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package connection

import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.http.*
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package connection.dom

import connection.WsClient
import kotlinx.browser.document
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope

0 comments on commit 121a757

Please sign in to comment.