feature clients

new frontend
This commit is contained in:
stefan 2025-09-25 13:03:15 +02:00
parent b8c008ddba
commit 0cc25cb108
32 changed files with 769 additions and 156 deletions

View File

@ -0,0 +1,44 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
js {
browser()
}
jvmToolchain(21)
sourceSets {
val commonMain by getting {
dependencies {
// Feature modules
implementation(project(":clients:ping-feature"))
// Shared modules
implementation(project(":clients:shared:common-ui"))
implementation(project(":clients:shared:navigation"))
// Compose dependencies
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
// ViewModel lifecycle
implementation(libs.androidx.lifecycle.viewmodelCompose)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}

View File

@ -0,0 +1,41 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import at.mocode.clients.shared.commonui.components.AppHeader
import at.mocode.clients.shared.commonui.components.AppScaffold
import at.mocode.clients.shared.commonui.theme.AppTheme
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel
@Composable
fun App() {
var currentScreen: AppScreen by remember { mutableStateOf(AppScreen.Home) }
AppTheme {
AppScaffold(
header = {
AppHeader(
title = "Meldestelle",
onNavigateToPing = { currentScreen = AppScreen.Ping }
)
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (currentScreen) {
is AppScreen.Home -> {
LandingScreen()
}
is AppScreen.Ping -> {
val pingViewModel: PingViewModel = viewModel()
PingScreen(viewModel = pingViewModel)
}
}
}
}
}
}

View File

@ -0,0 +1,99 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun LandingScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Willkommen bei Meldestelle",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Eine moderne, skalierbare Frontend-Architektur",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Diese Anwendung demonstriert eine \"Shell + Feature-Module\"-Architektur " +
"basierend auf Kotlin Multiplatform. Sie spiegelt die DDD-Struktur des Backends " +
"wider und ist als native Desktop-Anwendung (JVM) und Web-Anwendung (JS/Wasm) lauffähig.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "🚀 Technologien:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
TechItem("Kotlin Multiplatform")
TechItem("Jetpack Compose Multiplatform")
TechItem("Material Design 3")
TechItem("Ktor Client")
TechItem("Domain-Driven Design")
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Verwenden Sie das Ping Service Menü oben, um die API-Funktionalität zu testen.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun TechItem(text: String) {
Text(
text = "$text",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
)
}

View File

@ -0,0 +1,11 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import at.mocode.clients.app.App
import kotlinx.browser.document
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.body!!) {
App()
}
}

View File

@ -0,0 +1,12 @@
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import at.mocode.clients.app.App
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle - Desktop Application"
) {
App()
}
}

View File

@ -0,0 +1,10 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import at.mocode.clients.app.App
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow("Meldestelle - WASM Application") {
App()
}
}

View File

@ -1,3 +0,0 @@
package at.mocode
const val SERVER_PORT = 8081

View File

@ -1,9 +0,0 @@
package at.mocode
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}

View File

@ -1,7 +0,0 @@
package at.mocode
interface Platform {
val name: String
}
expect fun getPlatform(): Platform

View File

@ -1,10 +0,0 @@
package at.mocode.model
// Deprecated local DTOs are replaced by typealiases to the shared API contract.
// This preserves binary/source compatibility for existing imports while enforcing SSoT.
typealias PingResponse = at.mocode.ping.api.PingResponse
typealias EnhancedPingResponse = at.mocode.ping.api.EnhancedPingResponse
typealias HealthResponse = at.mocode.ping.api.HealthResponse

View File

@ -1,25 +0,0 @@
package at.mocode.ping.client
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import at.mocode.ping.api.PingApi
import at.mocode.ping.api.PingResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import at.mocode.service.getBaseUrl
class PingApiClient(
private val client: HttpClient,
baseUrl: String = getBaseUrl()
) : PingApi {
private val base = "$baseUrl/api/ping"
override suspend fun simplePing(): PingResponse = client.get("$base/simple").body()
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse =
client.get("$base/enhanced") { parameter("simulate", simulate) }.body()
override suspend fun healthCheck(): HealthResponse = client.get("$base/health").body()
}

View File

@ -1,43 +0,0 @@
package at.mocode.service
import at.mocode.model.EnhancedPingResponse
import at.mocode.model.HealthResponse
import at.mocode.model.PingResponse
import at.mocode.ping.client.PingApiClient
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
@Deprecated("Use PingApiClient directly for new code")
class PingService(
private val client: HttpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
install(HttpTimeout) {
requestTimeoutMillis = 10000
connectTimeoutMillis = 5000
}
}
) {
private val api = PingApiClient(client)
suspend fun ping(): Result<PingResponse> = runCatching { api.simplePing() }
suspend fun enhancedPing(simulate: Boolean = false): Result<EnhancedPingResponse> =
runCatching { api.enhancedPing(simulate) }
suspend fun health(): Result<HealthResponse> = runCatching { api.healthCheck() }
suspend fun testFailure(): Result<EnhancedPingResponse> = runCatching {
throw RuntimeException("Simulated failure for testing")
}
}
// Platform-specific base URL required by PingApiClient via getBaseUrl()
expect fun getBaseUrl(): String

View File

@ -1,12 +0,0 @@
package at.mocode
import kotlin.test.Test
import kotlin.test.assertEquals
class SharedCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}

View File

@ -1,7 +0,0 @@
package at.mocode
class JsPlatform: Platform {
override val name: String = "Web with Kotlin/JS"
}
actual fun getPlatform(): Platform = JsPlatform()

View File

@ -1,4 +0,0 @@
package at.mocode.service
// Use direct ping-service for JS Development - based on central.toml
actual fun getBaseUrl(): String = "http://localhost:8082"

View File

@ -1,7 +0,0 @@
package at.mocode
class JVMPlatform: Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
}
actual fun getPlatform(): Platform = JVMPlatform()

View File

@ -1,4 +0,0 @@
package at.mocode.service
// Use direct ping-service for JVM (Desktop) - based on central.toml
actual fun getBaseUrl(): String = "http://localhost:8082"

View File

@ -1,7 +0,0 @@
package at.mocode
class WasmPlatform: Platform {
override val name: String = "Web with Kotlin/Wasm"
}
actual fun getPlatform(): Platform = WasmPlatform()

View File

@ -1,4 +0,0 @@
package at.mocode.service
// Use direct ping-service for WASM Development - based on central.toml
actual fun getBaseUrl(): String = "http://localhost:8082"

View File

@ -1,9 +1,11 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
}
group = "at.mocode"
group = "at.mocode.clients"
version = "1.0.0"
kotlin {
@ -11,25 +13,35 @@ kotlin {
js {
browser()
}
// Keep WASM for dev since sources already present
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
jvmToolchain(21)
sourceSets {
val commonMain by getting {
dependencies {
// Contract from backend
implementation(projects.services.ping.pingApi)
// UI Kit
implementation(project(":clients:shared:common-ui"))
// Compose dependencies
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
// Ktor client for HTTP calls
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
// Coroutines and serialization
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
// ViewModel lifecycle
implementation(libs.androidx.lifecycle.viewmodelCompose)
}
}
val commonTest by getting {

View File

@ -0,0 +1,41 @@
package at.mocode.clients.pingfeature
import at.mocode.ping.api.PingApi
import at.mocode.ping.api.PingResponse
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class PingApiClient(
private val baseUrl: String = "http://localhost:8080"
) : PingApi {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
override suspend fun simplePing(): PingResponse {
return client.get("$baseUrl/api/ping/simple").body()
}
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse {
return client.get("$baseUrl/api/ping/enhanced") {
parameter("simulate", simulate)
}.body()
}
override suspend fun healthCheck(): HealthResponse {
return client.get("$baseUrl/api/ping/health").body()
}
}

View File

@ -0,0 +1,187 @@
package at.mocode.clients.pingfeature
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun PingScreen(viewModel: PingViewModel) {
val uiState = viewModel.uiState
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Ping Service",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { viewModel.performSimplePing() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Simple Ping")
}
Button(
onClick = { viewModel.performEnhancedPing() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Enhanced Ping")
}
Button(
onClick = { viewModel.performHealthCheck() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Health Check")
}
}
// Loading indicator
if (uiState.isLoading) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// Error message
uiState.errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Error",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = error,
color = MaterialTheme.colorScheme.onErrorContainer
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { viewModel.clearError() }
) {
Text("Dismiss")
}
}
}
}
// Simple Ping Response
uiState.simplePingResponse?.let { response ->
ResponseCard(
title = "Simple Ping Response",
status = response.status,
timestamp = response.timestamp,
service = response.service
)
}
// Enhanced Ping Response
uiState.enhancedPingResponse?.let { response ->
ResponseCard(
title = "Enhanced Ping Response",
status = response.status,
timestamp = response.timestamp,
service = response.service,
additionalInfo = mapOf(
"Circuit Breaker State" to response.circuitBreakerState,
"Response Time" to "${response.responseTime}ms"
)
)
}
// Health Response
uiState.healthResponse?.let { response ->
ResponseCard(
title = "Health Check Response",
status = response.status,
timestamp = response.timestamp,
service = response.service,
additionalInfo = mapOf(
"Healthy" to response.healthy.toString()
)
)
}
}
}
@Composable
private fun ResponseCard(
title: String,
status: String,
timestamp: String,
service: String,
additionalInfo: Map<String, String> = emptyMap()
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
InfoRow("Status", status)
InfoRow("Timestamp", timestamp)
InfoRow("Service", service)
additionalInfo.forEach { (key, value) ->
InfoRow(key, value)
}
}
}
}
@Composable
private fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "$label:",
fontWeight = FontWeight.Medium
)
Text(text = value)
}
}

View File

@ -0,0 +1,82 @@
package at.mocode.clients.pingfeature
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.ping.api.PingResponse
import at.mocode.ping.api.EnhancedPingResponse
import at.mocode.ping.api.HealthResponse
import kotlinx.coroutines.launch
data class PingUiState(
val isLoading: Boolean = false,
val simplePingResponse: PingResponse? = null,
val enhancedPingResponse: EnhancedPingResponse? = null,
val healthResponse: HealthResponse? = null,
val errorMessage: String? = null
)
class PingViewModel : ViewModel() {
private val apiClient = PingApiClient()
var uiState by mutableStateOf(PingUiState())
private set
fun performSimplePing() {
viewModelScope.launch {
uiState = uiState.copy(isLoading = true, errorMessage = null)
try {
val response = apiClient.simplePing()
uiState = uiState.copy(
isLoading = false,
simplePingResponse = response
)
} catch (e: Exception) {
uiState = uiState.copy(
isLoading = false,
errorMessage = "Simple ping failed: ${e.message}"
)
}
}
}
fun performEnhancedPing(simulate: Boolean = false) {
viewModelScope.launch {
uiState = uiState.copy(isLoading = true, errorMessage = null)
try {
val response = apiClient.enhancedPing(simulate)
uiState = uiState.copy(
isLoading = false,
enhancedPingResponse = response
)
} catch (e: Exception) {
uiState = uiState.copy(
isLoading = false,
errorMessage = "Enhanced ping failed: ${e.message}"
)
}
}
}
fun performHealthCheck() {
viewModelScope.launch {
uiState = uiState.copy(isLoading = true, errorMessage = null)
try {
val response = apiClient.healthCheck()
uiState = uiState.copy(
isLoading = false,
healthResponse = response
)
} catch (e: Exception) {
uiState = uiState.copy(
isLoading = false,
errorMessage = "Health check failed: ${e.message}"
)
}
}
}
fun clearError() {
uiState = uiState.copy(errorMessage = null)
}
}

View File

@ -0,0 +1,34 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
group = "at.mocode.clients.shared"
version = "1.0.0"
kotlin {
jvm()
js {
browser()
}
jvmToolchain(21)
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}

View File

@ -0,0 +1,29 @@
package at.mocode.clients.shared.commonui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun AppFooter() {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "© 2024 Meldestelle - Built with Kotlin Multiplatform",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}

View File

@ -0,0 +1,36 @@
package at.mocode.clients.shared.commonui.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontWeight
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppHeader(
title: String,
onNavigateToPing: (() -> Unit)? = null
) {
TopAppBar(
title = {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
},
actions = {
onNavigateToPing?.let { navigateAction ->
TextButton(
onClick = navigateAction
) {
Text("Ping Service")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}

View File

@ -0,0 +1,24 @@
package at.mocode.clients.shared.commonui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppScaffold(
header: @Composable () -> Unit = {
AppHeader(title = "Meldestelle")
},
footer: @Composable () -> Unit = {
AppFooter()
},
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
topBar = header,
bottomBar = footer,
content = content
)
}

View File

@ -0,0 +1,49 @@
package at.mocode.clients.shared.commonui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
// Define custom colors for the app
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
primaryContainer = Color(0xFFBBDEFB),
onPrimaryContainer = Color(0xFF0D47A1),
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
tertiary = Color(0xFF03A9F4),
background = Color(0xFFFAFAFA),
surface = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F)
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF0D47A1),
primaryContainer = Color(0xFF1565C0),
onPrimaryContainer = Color(0xFFBBDEFB),
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
tertiary = Color(0xFF03A9F4),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
onBackground = Color(0xFFE0E0E0),
onSurface = Color(0xFFE0E0E0)
)
@Composable
fun AppTheme(
darkTheme: Boolean = false, // For now, we'll default to light theme
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}

View File

@ -0,0 +1,32 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
group = "at.mocode.clients.shared"
version = "1.0.0"
kotlin {
jvm()
js {
browser()
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
jvmToolchain(21)
sourceSets {
val commonMain by getting {
dependencies {
// No specific dependencies needed for navigation routes
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}

View File

@ -0,0 +1,6 @@
package at.mocode.clients.shared.navigation
sealed class AppScreen {
data object Home : AppScreen()
data object Ping : AppScreen()
}

View File

@ -80,11 +80,14 @@ class RedisDistributedCacheEdgeCasesTest {
try {
cache.set("circular-reference", circularObject as Any)
logger.info { "Circular reference object was handled (possibly with Jackson's circular reference handling)" }
} catch (e: Exception) {
logger.info { "Circular reference object caused expected serialization issue: ${e::class.simpleName}" }
assertTrue(e is com.fasterxml.jackson.databind.JsonMappingException ||
e is StackOverflowError ||
e is RuntimeException, "Expected serialization-related exception")
} catch (t: Throwable) {
logger.info { "Circular reference object caused expected serialization issue: ${t::class.simpleName}" }
assertTrue(
t is com.fasterxml.jackson.databind.JsonMappingException ||
t is StackOverflowError ||
t is RuntimeException,
"Expected serialization-related exception"
)
}
// Test 2: Very deep nesting that might cause issues
@ -93,8 +96,8 @@ class RedisDistributedCacheEdgeCasesTest {
cache.set("deep-nested", deepObject as Any)
cache.get("deep-nested", DeeplyNestedObject::class.java)
logger.info { "Deep nested object serialized successfully" }
} catch (e: Exception) {
logger.info { "Deep nested object caused expected issues: ${e::class.simpleName}" }
} catch (t: Throwable) {
logger.info { "Deep nested object caused expected issues: ${t::class.simpleName}" }
}
// Verify that the cache remains stable after problematic serialization attempts

View File

@ -59,7 +59,10 @@ include(":services:ping:ping-api")
include(":services:ping:ping-service")
// Client modules
include(":clients:ping-client")
include(":clients:app")
include(":clients:ping-feature")
include(":clients:shared:common-ui")
include(":clients:shared:navigation")
// Documentation module
include(":docs")