diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3cb665b4..f483f5ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,8 +151,8 @@ dependencies { implementation(libs.okhttp) implementation(libs.okhttp.logging) implementation(libs.retrofit) - implementation(libs.retrofit.converter.gson) - implementation(libs.gson) + implementation(libs.retrofit.converter.kotlinx.serialization) + implementation(libs.kotlinx.serialization.json) // Image loading implementation(libs.coil.compose) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 40173527..9b09a8ca 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,11 +18,7 @@ -keep class com.m57.hermescontrol.ui.chat.MessageRole { *; } -keep class com.m57.hermescontrol.ui.chat.ToolStatus { *; } -# Gson itself --dontwarn com.google.gson.** --keep class com.google.gson.** { *; } - -# Generic type signatures used by Gson +# Generic type signatures used by serialization -keepattributes Signature -keepattributes *Annotation* -keepattributes EnclosingMethod diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index 5ccc273c..f09656da 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -4,6 +4,9 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys +import com.m57.hermescontrol.data.model.ConnectionProfile +import com.m57.hermescontrol.data.model.PinnedModel +import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.theme.BottomNavDisplayMode import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreset @@ -15,6 +18,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString /** * Singleton that manages encrypted storage of the Hermes dashboard token @@ -69,8 +74,6 @@ object AuthManager { private val _tokenFlow = MutableStateFlow(null) val tokenFlow: StateFlow = _tokenFlow.asStateFlow() - private val gson = com.m57.hermescontrol.data.remote.OkHttpProvider.gson - /** * Initialise the encrypted preferences. * Call this once from Application.onCreate() or MainActivity.onCreate(). @@ -160,14 +163,10 @@ object AuthManager { // ── Connection Profiles ────────────────────────────────────────────── - fun getConnectionProfiles(): List { + fun getConnectionProfiles(): List { val json = requirePrefs().getString(KEY_CONNECTION_PROFILES, null) ?: return emptyList() return try { - val type = - object : com.google.gson.reflect.TypeToken< - List, - >() {}.type - gson.fromJson(json, type) ?: emptyList() + OkHttpProvider.json.decodeFromString>(json) } catch (e: Exception) { if (com.m57.hermescontrol.BuildConfig.DEBUG) { android.util.Log.w("AuthManager", "Failed to parse connection profiles", e) @@ -176,21 +175,17 @@ object AuthManager { } } - fun saveConnectionProfiles(profiles: List) { - val json = gson.toJson(profiles) + fun saveConnectionProfiles(profiles: List) { + val json = OkHttpProvider.json.encodeToString(profiles) requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply() } // ── Pinned Models ──────────────────────────────────────────────────── - fun getPinnedModels(): List { + fun getPinnedModels(): List { val json = requirePrefs().getString(KEY_PINNED_MODELS, null) ?: return emptyList() return try { - val type = - object : com.google.gson.reflect.TypeToken< - List, - >() {}.type - gson.fromJson(json, type) ?: emptyList() + OkHttpProvider.json.decodeFromString>(json) } catch (e: Exception) { if (com.m57.hermescontrol.BuildConfig.DEBUG) { android.util.Log.w("AuthManager", "Failed to parse pinned models", e) @@ -200,7 +195,9 @@ object AuthManager { } fun savePinnedModels(pinned: List) { - val json = gson.toJson(pinned) + val json = + com.m57.hermescontrol.data.remote.OkHttpProvider.json + .encodeToString(pinned) requirePrefs().edit().putString(KEY_PINNED_MODELS, json).apply() } diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Achievement.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Achievement.kt index 8fc9c630..821a655d 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Achievement.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Achievement.kt @@ -1,68 +1,73 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class AchievementsResponse( val achievements: List? = emptyList(), - @SerializedName("unlocked_count") val unlockedCount: Int = 0, - @SerializedName("discovered_count") val discoveredCount: Int = 0, - @SerializedName("secret_count") val secretCount: Int = 0, - @SerializedName("total_count") val totalCount: Int = 0, - @SerializedName("is_stale") val isStale: Boolean = false, - @SerializedName("generated_at") val generatedAt: Long? = null, - @SerializedName("scan_meta") val scanMeta: ScanMeta? = null, + @SerialName("unlocked_count") val unlockedCount: Int = 0, + @SerialName("discovered_count") val discoveredCount: Int = 0, + @SerialName("secret_count") val secretCount: Int = 0, + @SerialName("total_count") val totalCount: Int = 0, + @SerialName("is_stale") val isStale: Boolean = false, + @SerialName("generated_at") val generatedAt: Long? = null, + @SerialName("scan_meta") val scanMeta: ScanMeta? = null, val error: String? = null, ) +@Serializable data class ScanMeta( val status: ScanStatus? = null, ) +@Serializable data class ScanStatus( val state: String = "idle", - @SerializedName("started_at") val startedAt: Long? = null, - @SerializedName("finished_at") val finishedAt: Long? = null, - @SerializedName("last_error") val lastError: String? = null, - @SerializedName("last_duration_ms") val lastDurationMs: Long? = null, - @SerializedName("run_count") val runCount: Int = 0, + @SerialName("started_at") val startedAt: Long? = null, + @SerialName("finished_at") val finishedAt: Long? = null, + @SerialName("last_error") val lastError: String? = null, + @SerialName("last_duration_ms") val lastDurationMs: Long? = null, + @SerialName("run_count") val runCount: Int = 0, ) +@Serializable data class RecentUnlock( val id: String, val name: String, - val description: String?, - val category: String?, - val kind: String?, - val icon: String?, + val description: String? = null, + val category: String? = null, + val kind: String? = null, + val icon: String? = null, val unlocked: Boolean, val discovered: Boolean, - val state: String?, - val tier: String?, - val progress: Double?, - @SerializedName("next_tier") val nextTier: String?, - @SerializedName("next_threshold") val nextThreshold: Double?, - @SerializedName("progress_pct") val progressPct: Double?, - @SerializedName("unlocked_at") val unlockedAt: Long?, - val secret: Boolean?, - val criteria: String?, + val state: String? = null, + val tier: String? = null, + val progress: Double? = null, + @SerialName("next_tier") val nextTier: String? = null, + @SerialName("next_threshold") val nextThreshold: Double? = null, + @SerialName("progress_pct") val progressPct: Double? = null, + @SerialName("unlocked_at") val unlockedAt: Long? = null, + val secret: Boolean? = null, + val criteria: String? = null, ) +@Serializable data class Achievement( val id: String, val name: String, - val description: String?, - val category: String?, - val kind: String?, - val icon: String?, + val description: String? = null, + val category: String? = null, + val kind: String? = null, + val icon: String? = null, val unlocked: Boolean, val discovered: Boolean, - val state: String?, - val tier: String?, - val progress: Double?, - @SerializedName("next_tier") val nextTier: String?, - @SerializedName("next_threshold") val nextThreshold: Double?, - @SerializedName("progress_pct") val progressPct: Double?, - @SerializedName("unlocked_at") val unlockedAt: Long?, - val secret: Boolean?, - val criteria: String?, + val state: String? = null, + val tier: String? = null, + val progress: Double? = null, + @SerialName("next_tier") val nextTier: String? = null, + @SerialName("next_threshold") val nextThreshold: Double? = null, + @SerialName("progress_pct") val progressPct: Double? = null, + @SerialName("unlocked_at") val unlockedAt: Long? = null, + val secret: Boolean? = null, + val criteria: String? = null, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ActionResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ActionResponse.kt index 5be6831b..ee4e5e6c 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ActionResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ActionResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class ActionResponse( val archive: String? = null, val name: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ActionStatusResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ActionStatusResponse.kt index 0b07dc14..332cf482 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ActionStatusResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ActionStatusResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class ActionStatusResponse( val exit_code: Int? = null, val lines: List? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Attachment.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Attachment.kt index 1980c6f2..1cc12774 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Attachment.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Attachment.kt @@ -1,4 +1,5 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable /** * Represents a file attached to a chat message. @@ -9,6 +10,7 @@ package com.m57.hermescontrol.data.model * @param size File size in bytes * @param localPath Optional local file system path for direct access */ +@Serializable data class Attachment( val uri: String, val name: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/CheckpointsResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/CheckpointsResponse.kt index 7eac4a0f..771d0199 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/CheckpointsResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/CheckpointsResponse.kt @@ -1,10 +1,13 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class CheckpointsResponse( val sessions: List? = null, val total_bytes: Long? = null, ) +@Serializable data class CheckpointSession( val session: String? = null, val files: Int? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Config.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Config.kt index b1b59c05..4424a547 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Config.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Config.kt @@ -1,22 +1,26 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement -import com.google.gson.JsonElement - +@Serializable data class RawConfigResponse( - val path: String?, - val yaml: String?, + val path: String? = null, + val yaml: String? = null, ) +@Serializable data class UpdateRawConfigRequest( val yaml_text: String, val profile: String? = null, ) +@Serializable data class ConfigSchemaResponse( val fields: Map, val category_order: List, ) +@Serializable data class SchemaField( val type: String, val description: String? = null, @@ -24,6 +28,7 @@ data class SchemaField( val options: List? = null, ) +@Serializable data class ConfigUpdateRequest( val config: Map, val profile: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ConnectionProfile.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ConnectionProfile.kt index 73732fd8..2fb94297 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ConnectionProfile.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ConnectionProfile.kt @@ -1,7 +1,8 @@ package com.m57.hermescontrol.data.model - +import kotlinx.serialization.Serializable import java.util.UUID +@Serializable data class ConnectionProfile( val id: String = UUID.randomUUID().toString(), val name: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/CredentialPoolResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/CredentialPoolResponse.kt index 2f6e2e73..a97b375a 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/CredentialPoolResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/CredentialPoolResponse.kt @@ -1,14 +1,18 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class CredentialPoolResponse( val providers: List? = null, ) +@Serializable data class CredentialPoolProvider( val provider: String? = null, val entries: List? = null, ) +@Serializable data class CredentialPoolEntry( val index: Int? = null, val label: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/CronJob.kt b/app/src/main/java/com/m57/hermescontrol/data/model/CronJob.kt index 4bfb5402..1c20efb5 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/CronJob.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/CronJob.kt @@ -1,17 +1,23 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +@Serializable data class CronJobRepeat( val times: Int? = null, val completed: Int = 0, ) +@Serializable data class CronJob( val id: String, val name: String, - val schedule: Any?, - val state: String?, - val last_run_status: String?, - val next_run: String?, + val schedule: JsonElement? = null, + val state: String? = null, + val last_run_status: String? = null, + val next_run: String? = null, val schedule_display: String? = null, val last_status: String? = null, val next_run_at: String? = null, @@ -36,8 +42,8 @@ data class CronJob( val scheduleText: String get() = when (schedule) { - is String -> schedule - is Map<*, *> -> (schedule["display"] as? String) ?: schedule_display ?: "" + is JsonPrimitive -> schedule.content + is JsonObject -> (schedule["display"] as? JsonPrimitive)?.content ?: schedule_display ?: "" else -> schedule_display ?: "" } diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/CronJobRequests.kt b/app/src/main/java/com/m57/hermescontrol/data/model/CronJobRequests.kt index 035642db..b5cc2e99 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/CronJobRequests.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/CronJobRequests.kt @@ -1,5 +1,8 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +@Serializable data class CreateCronJobRequest( val name: String = "", val schedule: String, @@ -17,6 +20,7 @@ data class CreateCronJobRequest( val repeat: Int? = null, ) +@Serializable data class UpdateCronJobRequest( - val updates: Map, + val updates: Map, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/CuratorResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/CuratorResponse.kt index bbaf6bbe..70bdf3c2 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/CuratorResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/CuratorResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class CuratorResponse( val enabled: Boolean? = null, val paused: Boolean? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/DebugShareResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/DebugShareResponse.kt index 2ae1c565..14be3d2d 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/DebugShareResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/DebugShareResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class DebugShareResponse( val ok: Boolean? = null, val urls: Map? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/DoctorResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/DoctorResponse.kt index 980b6afe..db51b394 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/DoctorResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/DoctorResponse.kt @@ -1,7 +1,9 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class DoctorResponse( val ok: Boolean, - val pid: Int?, - val name: String?, + val pid: Int? = null, + val name: String? = null, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/EnvVarConfig.kt b/app/src/main/java/com/m57/hermescontrol/data/model/EnvVarConfig.kt index 293be6c6..910e0409 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/EnvVarConfig.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/EnvVarConfig.kt @@ -1,32 +1,37 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class EnvVarConfig( - @SerializedName("is_set") val isSet: Boolean, - @SerializedName("redacted_value") val redactedValue: String?, - val description: String?, - val url: String?, - val category: String?, - @SerializedName("is_password") val isPassword: Boolean, + @SerialName("is_set") val isSet: Boolean, + @SerialName("redacted_value") val redactedValue: String? = null, + val description: String? = null, + val url: String? = null, + val category: String? = null, + @SerialName("is_password") val isPassword: Boolean, ) +@Serializable data class EnvVarRevealRequest( val key: String, val profile: String? = null, ) +@Serializable data class EnvVarRevealResponse( val key: String, val value: String, ) +@Serializable data class EnvVarUpdate( val key: String, val value: String, val profile: String? = null, ) +@Serializable data class EnvVarDeleteRequest( val key: String, val profile: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/HookResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/HookResponse.kt index e48a7855..5f4f4b31 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/HookResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/HookResponse.kt @@ -1,10 +1,13 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class HookResponse( val hooks: List? = null, val valid_events: List? = null, ) +@Serializable data class HookEntry( val event: String? = null, val matcher: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Kanban.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Kanban.kt index 67b9b73d..592c4b50 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Kanban.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Kanban.kt @@ -1,37 +1,43 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class KanbanBoard( - @SerializedName("slug") val id: String, + @SerialName("slug") val id: String, val name: String, - val description: String?, + val description: String? = null, ) +@Serializable data class KanbanTask( val id: String, val title: String, - @SerializedName("body") val description: String?, + @SerialName("body") val description: String? = null, val status: String, - @SerializedName("assignee") val assignedTo: String?, + @SerialName("assignee") val assignedTo: String? = null, ) +@Serializable data class CreateTaskBody( val title: String, - val body: String?, + val body: String? = null, ) +@Serializable data class KanbanColumn( val name: String, val tasks: List, ) +@Serializable data class KanbanBoardResponse( val columns: List, - val assignees: List?, - val tenants: List?, + val assignees: List? = null, + val tenants: List? = null, ) +@Serializable data class KanbanBoardsResponse( val boards: List, val current: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/LogResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/LogResponse.kt index bbdefdcf..b008b7ea 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/LogResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/LogResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class LogResponse( val lines: List? = null, val logs: List? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt b/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt index 5adda21e..55d681e0 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt @@ -1,25 +1,30 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class McpServersResponse( val servers: List, ) +@Serializable data class McpServer( val name: String, - val transport: String?, - val url: String?, - val command: String?, - val args: List?, - val env: Map?, + val transport: String? = null, + val url: String? = null, + val command: String? = null, + val args: List? = null, + val env: Map? = null, val enabled: Boolean, val status: String? = null, val error: String? = null, ) +@Serializable data class McpServerToggleRequest( val enabled: Boolean, ) +@Serializable data class AddMcpServerRequest( val name: String, val url: String? = null, @@ -28,10 +33,12 @@ data class AddMcpServerRequest( val env: Map? = null, ) +@Serializable data class McpCatalogResponse( val entries: List, ) +@Serializable data class McpCatalogEntry( val name: String, val description: String? = null, @@ -42,6 +49,7 @@ data class McpCatalogEntry( val env: List? = null, ) +@Serializable data class McpCatalogEnvVar( val key: String, val label: String? = null, @@ -49,6 +57,7 @@ data class McpCatalogEnvVar( val required: Boolean = false, ) +@Serializable data class McpCatalogInstallRequest( val name: String, val env: Map? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/MemoryResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/MemoryResponse.kt index 273e6062..9b69de0c 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/MemoryResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/MemoryResponse.kt @@ -1,10 +1,13 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class MemoryResponse( val active: String? = null, val builtin_files: BuiltinFileSizes? = null, ) +@Serializable data class BuiltinFileSizes( val memory: Long? = null, val user: Long? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/MessagingPlatform.kt b/app/src/main/java/com/m57/hermescontrol/data/model/MessagingPlatform.kt index c72997bc..465b5451 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/MessagingPlatform.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/MessagingPlatform.kt @@ -1,57 +1,63 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class MessagingPlatform( val id: String, val name: String, val description: String? = null, - @SerializedName("docs_url") val docsUrl: String? = null, + @SerialName("docs_url") val docsUrl: String? = null, val enabled: Boolean, val configured: Boolean, - @SerializedName("gateway_running") val gatewayRunning: Boolean = false, + @SerialName("gateway_running") val gatewayRunning: Boolean = false, val state: String = "disabled", - @SerializedName("error_code") val errorCode: String? = null, - @SerializedName("error_message") val errorMessage: String? = null, - @SerializedName("env_vars") val envVars: List? = null, - @SerializedName("updated_at") val updatedAt: String? = null, - @SerializedName("home_channel") val homeChannel: HomeChannelInfo? = null, + @SerialName("error_code") val errorCode: String? = null, + @SerialName("error_message") val errorMessage: String? = null, + @SerialName("env_vars") val envVars: List? = null, + @SerialName("updated_at") val updatedAt: String? = null, + @SerialName("home_channel") val homeChannel: HomeChannelInfo? = null, ) +@Serializable data class HomeChannelInfo( val platform: String, - @SerializedName("chat_id") val chatId: String, + @SerialName("chat_id") val chatId: String, val name: String, - @SerializedName("thread_id") val threadId: String? = null, + @SerialName("thread_id") val threadId: String? = null, ) +@Serializable data class EnvVarField( val key: String, val required: Boolean, - @SerializedName("is_set") val isSet: Boolean, - @SerializedName("redacted_value") val redactedValue: String? = null, + @SerialName("is_set") val isSet: Boolean, + @SerialName("redacted_value") val redactedValue: String? = null, val description: String? = null, val prompt: String? = null, val help: String? = null, - @SerializedName("is_password") val isPassword: Boolean, + @SerialName("is_password") val isPassword: Boolean, val advanced: Boolean = false, val url: String? = null, - @SerializedName("expires_at") val expiresAt: String? = null, + @SerialName("expires_at") val expiresAt: String? = null, ) +@Serializable data class MessagingPlatformResponse( - @SerializedName("env_path") val envPath: String, - @SerializedName("gateway_start_command") val gatewayStartCommand: String, + @SerialName("env_path") val envPath: String, + @SerialName("gateway_start_command") val gatewayStartCommand: String, val platforms: List, ) +@Serializable data class MessagingPlatformUpdate( val enabled: Boolean? = null, val env: Map = emptyMap(), - @SerializedName("clear_env") val clearEnv: List = emptyList(), + @SerialName("clear_env") val clearEnv: List = emptyList(), val profile: String? = null, ) +@Serializable data class MessagingPlatformTestResult( val ok: Boolean, val state: String, @@ -60,37 +66,42 @@ data class MessagingPlatformTestResult( // ── Telegram onboarding ───────────────────────────────────────────────── +@Serializable data class TelegramOnboardingStartRequest( - @SerializedName("bot_name") val botName: String = "Hermes Agent", + @SerialName("bot_name") val botName: String = "Hermes Agent", ) +@Serializable data class TelegramOnboardingStartResponse( - @SerializedName("pairing_id") val pairingId: String, - @SerializedName("suggested_username") val suggestedUsername: String, - @SerializedName("deep_link") val deepLink: String, - @SerializedName("qr_payload") val qrPayload: String, - @SerializedName("expires_at") val expiresAt: String, + @SerialName("pairing_id") val pairingId: String, + @SerialName("suggested_username") val suggestedUsername: String, + @SerialName("deep_link") val deepLink: String, + @SerialName("qr_payload") val qrPayload: String, + @SerialName("expires_at") val expiresAt: String, ) +@Serializable data class TelegramOnboardingStatusResponse( val status: String, - @SerializedName("bot_username") val botUsername: String? = null, - @SerializedName("owner_user_id") val ownerUserId: String? = null, - @SerializedName("expires_at") val expiresAt: String? = null, + @SerialName("bot_username") val botUsername: String? = null, + @SerialName("owner_user_id") val ownerUserId: String? = null, + @SerialName("expires_at") val expiresAt: String? = null, ) +@Serializable data class TelegramOnboardingApplyRequest( - @SerializedName("allowed_user_ids") val allowedUserIds: List, + @SerialName("allowed_user_ids") val allowedUserIds: List, val profile: String? = null, ) +@Serializable data class TelegramOnboardingApplyResponse( val ok: Boolean, val platform: String, - @SerializedName("bot_username") val botUsername: String? = null, - @SerializedName("needs_restart") val needsRestart: Boolean, - @SerializedName("restart_started") val restartStarted: Boolean? = null, - @SerializedName("restart_action") val restartAction: String? = null, - @SerializedName("restart_pid") val restartPid: Int? = null, - @SerializedName("restart_error") val restartError: String? = null, + @SerialName("bot_username") val botUsername: String? = null, + @SerialName("needs_restart") val needsRestart: Boolean, + @SerialName("restart_started") val restartStarted: Boolean? = null, + @SerialName("restart_action") val restartAction: String? = null, + @SerialName("restart_pid") val restartPid: Int? = null, + @SerialName("restart_error") val restartError: String? = null, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt index 4f71181c..ec25c2af 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class AuxiliaryTaskAssignment( val task: String, val provider: String, @@ -7,21 +9,25 @@ data class AuxiliaryTaskAssignment( val base_url: String? = null, ) +@Serializable data class AuxiliaryModelsResponse( val tasks: List, val main: MainModelAssignment, ) +@Serializable data class MainModelAssignment( val provider: String, val model: String, ) +@Serializable data class MoaModelSlot( val provider: String, val model: String, ) +@Serializable data class MoaConfigPreset( val reference_models: List, val aggregator: MoaModelSlot, @@ -31,6 +37,7 @@ data class MoaConfigPreset( val enabled: Boolean = true, ) +@Serializable data class MoaConfigResponse( val default_preset: String = "", val active_preset: String = "", @@ -43,6 +50,7 @@ data class MoaConfigResponse( val enabled: Boolean = true, ) +@Serializable data class ModelAssignmentRequest( val confirm_expensive_model: Boolean = false, val scope: String, @@ -52,6 +60,7 @@ data class ModelAssignmentRequest( val task: String? = null, ) +@Serializable data class ModelAssignmentResponse( val confirm_message: String? = null, val confirm_required: Boolean? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ModelOption.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ModelOption.kt index ded62f08..13b46171 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ModelOption.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ModelOption.kt @@ -1,22 +1,26 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class ModelOptionsResponse( val providers: List, ) +@Serializable data class ModelProvider( val slug: String, val name: String, - val is_current: Boolean?, - val is_user_defined: Boolean?, - val models: List?, - val total_models: Int?, - val source: String?, - val authenticated: Boolean?, - val auth_type: String?, - val warning: String?, + val is_current: Boolean? = null, + val is_user_defined: Boolean? = null, + val models: List? = null, + val total_models: Int? = null, + val source: String? = null, + val authenticated: Boolean? = null, + val auth_type: String? = null, + val warning: String? = null, ) +@Serializable data class PinnedModel( val providerSlug: String, val modelName: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Pairing.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Pairing.kt index ac1bf08a..ca45442c 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Pairing.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Pairing.kt @@ -1,24 +1,29 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class PairingResponse( val pending: List, val approved: List, ) +@Serializable data class PairingItem( val platform: String, - val user_id: String?, - val username: String?, - val display_name: String?, - val code: String?, - val created_at: String?, + val user_id: String? = null, + val username: String? = null, + val display_name: String? = null, + val code: String? = null, + val created_at: String? = null, ) +@Serializable data class PairingApproveRequest( val platform: String, val code: String, ) +@Serializable data class PairingRevokeRequest( val platform: String, val user_id: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/PluginInfo.kt b/app/src/main/java/com/m57/hermescontrol/data/model/PluginInfo.kt index 1316fec9..1ca609bd 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/PluginInfo.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/PluginInfo.kt @@ -1,20 +1,21 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class PluginInfo( val name: String, - val description: String?, - val version: String?, - val source: String?, - @SerializedName("runtime_status") val runtimeStatus: String?, - @SerializedName("has_dashboard_manifest") val hasDashboardManifest: Boolean = false, - @SerializedName("dashboard_manifest") val dashboardManifest: PluginManifestData? = null, - @SerializedName("can_remove") val canRemove: Boolean = false, - @SerializedName("can_update_git") val canUpdateGit: Boolean = false, - @SerializedName("auth_required") val authRequired: Boolean = false, - @SerializedName("auth_command") val authCommand: String? = null, - @SerializedName("user_hidden") val userHidden: Boolean = false, + val description: String? = null, + val version: String? = null, + val source: String? = null, + @SerialName("runtime_status") val runtimeStatus: String? = null, + @SerialName("has_dashboard_manifest") val hasDashboardManifest: Boolean = false, + @SerialName("dashboard_manifest") val dashboardManifest: PluginManifestData? = null, + @SerialName("can_remove") val canRemove: Boolean = false, + @SerialName("can_update_git") val canUpdateGit: Boolean = false, + @SerialName("auth_required") val authRequired: Boolean = false, + @SerialName("auth_command") val authCommand: String? = null, + @SerialName("user_hidden") val userHidden: Boolean = false, ) { val enabled: Boolean get() = runtimeStatus.equals("enabled", ignoreCase = true) @@ -25,6 +26,7 @@ data class PluginInfo( runtimeStatus.equals("disabled", ignoreCase = true) } +@Serializable data class PluginManifestData( val name: String? = null, val label: String? = null, @@ -35,10 +37,11 @@ data class PluginManifestData( val slots: List? = null, val entry: String? = null, val css: String? = null, - @SerializedName("has_api") val hasApi: Boolean = false, + @SerialName("has_api") val hasApi: Boolean = false, val source: String? = null, ) +@Serializable data class PluginTabInfo( val path: String? = null, val position: String? = null, @@ -46,6 +49,7 @@ data class PluginTabInfo( val hidden: Boolean = false, ) +@Serializable data class DashboardPluginMeta( val name: String? = null, val label: String? = null, @@ -53,31 +57,36 @@ data class DashboardPluginMeta( val tab: PluginTabInfo? = null, ) +@Serializable data class PluginsHubProviders( - @SerializedName("memory_provider") val memoryProvider: String? = null, - @SerializedName("memory_options") val memoryOptions: List = emptyList(), - @SerializedName("context_engine") val contextEngine: String? = null, - @SerializedName("context_options") val contextOptions: List = emptyList(), + @SerialName("memory_provider") val memoryProvider: String? = null, + @SerialName("memory_options") val memoryOptions: List = emptyList(), + @SerialName("context_engine") val contextEngine: String? = null, + @SerialName("context_options") val contextOptions: List = emptyList(), ) +@Serializable data class ProviderOption( val name: String, val description: String? = null, ) +@Serializable data class PluginsHubResponse( val plugins: List, - @SerializedName("orphan_dashboard_plugins") val orphanDashboardPlugins: List = emptyList(), + @SerialName("orphan_dashboard_plugins") val orphanDashboardPlugins: List = emptyList(), val providers: PluginsHubProviders? = null, ) +@Serializable data class AgentPluginInstallBody( val identifier: String, val force: Boolean = false, val enable: Boolean = true, ) +@Serializable data class PluginProvidersPutRequest( - @SerializedName("memory_provider") val memoryProvider: String? = null, - @SerializedName("context_engine") val contextEngine: String? = null, + @SerialName("memory_provider") val memoryProvider: String? = null, + @SerialName("context_engine") val contextEngine: String? = null, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/PortalResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/PortalResponse.kt index f606783b..b8392299 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/PortalResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/PortalResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class PortalResponse( val logged_in: Boolean? = null, val portal_url: String? = null, @@ -9,6 +11,7 @@ data class PortalResponse( val features: List? = null, ) +@Serializable data class PortalFeature( val label: String? = null, val state: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt index cdd90e33..dd550a1f 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt @@ -1,9 +1,12 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class ProfilesResponse( val profiles: List, ) +@Serializable data class ProfileInfo( val name: String, val path: String? = null, @@ -17,46 +20,55 @@ data class ProfileInfo( val description_auto: Boolean? = null, ) +@Serializable data class ActiveProfileResponse( val active: String, - val current: String?, + val current: String? = null, ) +@Serializable data class SetActiveProfileRequest( val name: String, ) +@Serializable data class ProfileSoulResponse( val content: String, ) +@Serializable data class UpdateProfileSoulRequest( val content: String, ) +@Serializable data class UpdateProfileModelRequest( val provider: String, val model: String, ) +@Serializable data class CloneProfileRequest( val name: String, ) +@Serializable data class UpdateProfileDescriptionRequest( val description: String, ) +@Serializable data class CreateProfileRequest( val name: String, - val description: String?, - val provider: String?, - val model: String?, - val mcp_servers: List?, - val keep_skills: Boolean?, - val hub_skills: List?, + val description: String? = null, + val provider: String? = null, + val model: String? = null, + val mcp_servers: List? = null, + val keep_skills: Boolean? = null, + val hub_skills: List? = null, ) +@Serializable data class McpServerConfigInput( val name: String, val transport: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SaveSkillContentRequest.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SaveSkillContentRequest.kt index 0e1b6940..d163b08a 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SaveSkillContentRequest.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SaveSkillContentRequest.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class SaveSkillContentRequest( val name: String, val content: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt index ef7cbb91..32f6cbe5 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class SessionListResponse( val sessions: List, val total: Int = 0, @@ -7,35 +9,41 @@ data class SessionListResponse( val offset: Int = 0, ) +@Serializable data class SessionInfo( val id: String, - val title: String?, - val created_at: String?, - val message_count: Int?, - val status: String?, + val title: String? = null, + val created_at: String? = null, + val message_count: Int? = null, + val status: String? = null, val preview: String? = null, val started_at: Double? = null, val source: String? = null, ) +@Serializable data class SessionStatsResponse( val total: Int = 0, val active: Int = 0, ) +@Serializable data class SessionRenameRequest( val title: String, ) +@Serializable data class BulkDeleteRequest( val ids: List, val delete_all: Boolean = false, ) +@Serializable data class PruneRequest( val days: Int, ) +@Serializable data class SessionPromptResponse( val prompt: String? = null, val id: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SessionMessagesResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SessionMessagesResponse.kt index b16c4d36..735bef87 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SessionMessagesResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SessionMessagesResponse.kt @@ -1,12 +1,20 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +@Serializable data class SessionMessagesResponse( val messages: List, ) +@Serializable data class SessionMessage( - val role: String?, - val content: String?, - val timestamp: String?, - val type: String?, -) + val role: String? = null, + val content: String? = null, + val timestamp: JsonElement? = null, + val type: String? = null, +) { + val timestampText: String? + get() = (timestamp as? JsonPrimitive)?.content +} diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Skill.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Skill.kt index 27ba1712..22e0e25b 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Skill.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Skill.kt @@ -1,11 +1,12 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -import com.google.gson.annotations.SerializedName - +@Serializable data class Skill( val name: String, - val description: String?, - val category: String?, + val description: String? = null, + val category: String? = null, val enabled: Boolean, val content: String? = null, val source: String? = null, // built-in, hub, optional @@ -13,40 +14,46 @@ data class Skill( val linked_files: List? = null, ) +@Serializable data class HubSkill( val name: String, - val description: String?, - val source: String?, + val description: String? = null, + val source: String? = null, val category: String? = null, val identifier: String? = null, - @SerializedName("trust_level") val trustLevel: String? = null, + @SerialName("trust_level") val trustLevel: String? = null, val repo: String? = null, val tags: List? = null, ) +@Serializable data class SkillHubSearchResponse( val results: List, - @SerializedName("source_counts") val sourceCounts: Map? = null, - @SerializedName("timed_out") val timedOut: List? = null, + @SerialName("source_counts") val sourceCounts: Map? = null, + @SerialName("timed_out") val timedOut: List? = null, val installed: Map? = null, ) +@Serializable data class SkillHubInstalledEntry( val name: String? = null, - @SerializedName("trust_level") val trustLevel: String? = null, - @SerializedName("scan_verdict") val scanVerdict: String? = null, + @SerialName("trust_level") val trustLevel: String? = null, + @SerialName("scan_verdict") val scanVerdict: String? = null, ) +@Serializable data class SkillHubInstallRequest( val identifier: String, val profile: String? = null, ) +@Serializable data class SkillHubUninstallRequest( val name: String, val profile: String? = null, ) +@Serializable data class SkillScanResponse( val identifier: String, val passed: Boolean? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SkillContentResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SkillContentResponse.kt index 31815bf2..f07e80b8 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SkillContentResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SkillContentResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class SkillContentResponse( val name: String, val content: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/StatusResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/StatusResponse.kt index ab94daa5..f78fabe4 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/StatusResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/StatusResponse.kt @@ -1,14 +1,17 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class StatusResponse( - val version: String?, - val gateway_running: Boolean?, - val active_sessions: Int?, - val auth_required: Boolean?, - val gateway_platforms: Map?, + val version: String? = null, + val gateway_running: Boolean? = null, + val active_sessions: Int? = null, + val auth_required: Boolean? = null, + val gateway_platforms: Map? = null, ) +@Serializable data class PlatformStatus( - val state: String?, + val state: String? = null, val error_code: String? = null, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SystemStatsResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SystemStatsResponse.kt index 3ed6f3ca..2c371aab 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SystemStatsResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SystemStatsResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class SystemStatsResponse( val os: String? = null, val os_release: String? = null, @@ -20,6 +22,7 @@ data class SystemStatsResponse( val process: ProcessStats? = null, ) +@Serializable data class MemoryStats( val total: Long? = null, val available: Long? = null, @@ -27,6 +30,7 @@ data class MemoryStats( val percent: Double? = null, ) +@Serializable data class DiskStats( val total: Long? = null, val used: Long? = null, @@ -34,6 +38,7 @@ data class DiskStats( val percent: Double? = null, ) +@Serializable data class ProcessStats( val pid: Int? = null, val rss: Long? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ToggleSkillRequest.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ToggleSkillRequest.kt index 75f7fcb6..6fd16818 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ToggleSkillRequest.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ToggleSkillRequest.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class ToggleSkillRequest( val name: String, val enabled: Boolean, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Toolset.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Toolset.kt index 3445ae8e..0245b1ab 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Toolset.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Toolset.kt @@ -1,15 +1,18 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class Toolset( val name: String, - val label: String?, - val description: String?, + val label: String? = null, + val description: String? = null, val enabled: Boolean, - val available: Boolean?, - val configured: Boolean?, - val tools: List?, + val available: Boolean? = null, + val configured: Boolean? = null, + val tools: List? = null, ) +@Serializable data class ToolsetToggleRequest( val enabled: Boolean, val profile: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/UpdateCheckResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/UpdateCheckResponse.kt index df7f8689..65fb8f7a 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/UpdateCheckResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/UpdateCheckResponse.kt @@ -1,5 +1,7 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class UpdateCheckResponse( val install_method: String? = null, val current_version: String? = null, diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Webhook.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Webhook.kt index 9d2eb781..b6518284 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Webhook.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Webhook.kt @@ -1,25 +1,29 @@ package com.m57.hermescontrol.data.model +import kotlinx.serialization.Serializable +@Serializable data class WebhooksResponse( val enabled: Boolean, - val base_url: String?, - val subscriptions: List?, + val base_url: String? = null, + val subscriptions: List? = null, ) +@Serializable data class WebhookSubscription( val name: String, - val description: String?, - val events: List?, - val deliver: String?, - val deliver_only: Boolean?, - val prompt: String?, - val skills: List?, - val created_at: String?, + val description: String? = null, + val events: List? = null, + val deliver: String? = null, + val deliver_only: Boolean? = null, + val prompt: String? = null, + val skills: List? = null, + val created_at: String? = null, val url: String, - val secret_set: Boolean?, - val enabled: Boolean?, + val secret_set: Boolean? = null, + val enabled: Boolean? = null, ) +@Serializable data class CreateWebhookRequest( val name: String, val description: String? = null, @@ -32,14 +36,17 @@ data class CreateWebhookRequest( val secret: String? = null, ) +@Serializable data class WebhooksToggleRequest( val enabled: Boolean, ) +@Serializable data class WebhookToggleSubscriptionRequest( val enabled: Boolean, ) +@Serializable data class DeleteWebhookResponse( val ok: Boolean, ) diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/ApiClient.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/ApiClient.kt index 075ec4ee..bcb5d40f 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/ApiClient.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/ApiClient.kt @@ -4,9 +4,10 @@ import com.m57.hermescontrol.BuildConfig import com.m57.hermescontrol.data.local.AuthManager import okhttp3.CertificatePinner import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.kotlinx.serialization.asConverterFactory /** * Provides a Retrofit-backed [HermesApiService]. @@ -71,7 +72,7 @@ object ApiClient { .Builder() .baseUrl("http://$host:$port/") .client(tempOkHttp) - .addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson)) + .addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType())) .build() return tempRetrofit.create(HermesApiService::class.java) @@ -135,7 +136,7 @@ object ApiClient { .Builder() .baseUrl(AuthManager.baseUrl()) .client(okHttp) - .addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson)) + .addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType())) .build() .also { retrofit = it } diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt index 9950e430..fcff05b6 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.data.remote -import com.google.gson.JsonElement import com.m57.hermescontrol.data.model.AchievementsResponse import com.m57.hermescontrol.data.model.ActionResponse import com.m57.hermescontrol.data.model.ActionStatusResponse @@ -91,6 +90,7 @@ import com.m57.hermescontrol.data.model.WebhookSubscription import com.m57.hermescontrol.data.model.WebhookToggleSubscriptionRequest import com.m57.hermescontrol.data.model.WebhooksResponse import com.m57.hermescontrol.data.model.WebhooksToggleRequest +import kotlinx.serialization.json.JsonElement import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt index 3fc9fd89..6512ec99 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt @@ -1,7 +1,8 @@ package com.m57.hermescontrol.data.remote -import com.google.gson.JsonParser import kotlinx.coroutines.delay +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import retrofit2.Response import java.io.IOException @@ -49,11 +50,8 @@ fun mapHttpError( val detail = if (errorBody != null) { try { - val parsed = - com.google.gson.JsonParser - .parseString(errorBody) - .asJsonObject - parsed.get("detail")?.asString + val parsed = OkHttpProvider.json.parseToJsonElement(errorBody).jsonObject + parsed["detail"]?.jsonPrimitive?.content } catch (_: Exception) { null } diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/OkHttpProvider.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/OkHttpProvider.kt index 1b7e6109..8f2b8550 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/OkHttpProvider.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/OkHttpProvider.kt @@ -1,7 +1,8 @@ package com.m57.hermescontrol.data.remote -import com.google.gson.Gson -import com.google.gson.GsonBuilder +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy import okhttp3.ConnectionPool import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit @@ -42,6 +43,10 @@ object OkHttpProvider { .followRedirects(false) .build() - // Shared Gson — thread-safe, zero-config - val gson: Gson = GsonBuilder().create() + @OptIn(ExperimentalSerializationApi::class) + val json: Json = + Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + } } diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/CommandModels.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/CommandModels.kt index 282a8b69..121ce4f1 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/CommandModels.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/CommandModels.kt @@ -1,9 +1,12 @@ package com.m57.hermescontrol.data.ws +import kotlinx.serialization.Serializable + /** * Response from `commands.catalog` RPC — full catalog of available * slash commands, subcommands, and alias mappings. */ +@Serializable data class CommandCatalog( val pairs: List> = emptyList(), val sub: Map> = emptyMap(), @@ -16,6 +19,7 @@ data class CommandCatalog( /** * A named category within the command catalog. */ +@Serializable data class CommandCategory( val name: String, val pairs: List>, diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt index 25c07a0b..3bc025fc 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt @@ -24,12 +24,13 @@ object EventParser { return if (response.error != null) { WsEvent.RpcError(id, response.error) } else { - WsEvent.RpcResult(id, response.result) + WsEvent.RpcResult(id, response.result?.toAny()) } } // ── Notification / event (no id, has method) ───────────────────── - val params = response.params ?: return WsEvent.Unknown(rawJson) + @Suppress("UNCHECKED_CAST") + val params = response.params?.toAny() as? Map ?: return WsEvent.Unknown(rawJson) val eventType = params["type"] as? String ?: return WsEvent.Unknown(rawJson) @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt index c89b2845..65a4650f 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response @@ -123,7 +125,7 @@ object HermesWsClient { .buffer(Channel.BUFFERED) .map { text -> try { - val rpc = OkHttpProvider.gson.fromJson(text, JsonRpcResponse::class.java) + val rpc = OkHttpProvider.json.decodeFromString(text) EventParser.parse(rpc, text) } catch (e: Exception) { Log.e(TAG, "Failed to parse message", e) @@ -231,8 +233,8 @@ object HermesWsClient { ): String { val id = requestId.incrementAndGet().toString() onSent?.invoke(id) - val request = JsonRpcRequest(id = id, method = method, params = params) - val json = OkHttpProvider.gson.toJson(request) + val request = JsonRpcRequest(id = id, method = method, params = params.mapValues { it.value.toJsonElement() }) + val json = OkHttpProvider.json.encodeToString(request) if (BuildConfig.DEBUG) Log.d(TAG, "→ $json") val ws = webSocket if (ws != null && connected.get()) { diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/JsonRpcModels.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/JsonRpcModels.kt index 79e56a58..f5ebe880 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/JsonRpcModels.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/JsonRpcModels.kt @@ -1,32 +1,77 @@ package com.m57.hermescontrol.data.ws +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull + /** * Outgoing JSON-RPC 2.0 request sent over the WebSocket. */ +@Serializable data class JsonRpcRequest( val jsonrpc: String = "2.0", val id: String, val method: String, - val params: Map = emptyMap(), + val params: Map = emptyMap(), ) /** * Incoming JSON-RPC 2.0 response **or** server-pushed notification. - * - * - RPC result/error → [id] is non-null, [result] or [error] is populated. - * - Event notification → [id] is null, [method] is `"event"`, event data in [params]. */ +@Serializable data class JsonRpcResponse( - val jsonrpc: String?, - val id: String?, - val result: Any?, - val error: JsonRpcError?, - val method: String?, - val params: Map?, + val jsonrpc: String? = null, + val id: String? = null, + val result: JsonElement? = null, + val error: JsonRpcError? = null, + val method: String? = null, + val params: JsonObject? = null, ) +@Serializable data class JsonRpcError( val code: Int, val message: String, - val data: Any? = null, + val data: JsonElement? = null, ) + +fun Any?.toJsonElement(): JsonElement = + when (this) { + null -> JsonNull + is JsonElement -> this + is Boolean -> JsonPrimitive(this) + is Number -> JsonPrimitive(this) + is String -> JsonPrimitive(this) + is Map<*, *> -> JsonObject(this.map { it.key.toString() to it.value.toJsonElement() }.toMap()) + is List<*> -> JsonArray(this.map { it.toJsonElement() }) + else -> JsonPrimitive(this.toString()) + } + +fun JsonElement.toAny(): Any? = + when (this) { + is JsonNull -> { + null + } + + is JsonPrimitive -> { + if (isString) { + content + } else { + booleanOrNull ?: doubleOrNull ?: longOrNull ?: content + } + } + + is JsonObject -> { + mapValues { it.value.toAny() } + } + + is JsonArray -> { + map { it.toAny() } + } + } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 3f4a0add..1beec0e0 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -82,6 +82,7 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.Attachment +import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.theme.AssistantBubble import com.m57.hermescontrol.theme.AssistantBubbleLight import com.m57.hermescontrol.theme.StatusGreen @@ -92,6 +93,14 @@ import com.m57.hermescontrol.theme.ToolChipColorLight import com.m57.hermescontrol.theme.UserBubble import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -537,9 +546,7 @@ fun parseToolOutput( val trimmed = content.trim() if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null return try { - val element = - com.google.gson.JsonParser - .parseString(trimmed) + val element = OkHttpProvider.json.parseToJsonElement(trimmed) if (!element.isJsonObject) return null val obj = element.asJsonObject @@ -2301,3 +2308,23 @@ private fun InlineAttachment( } } } + +// Extensions for backward compatibility with Gson API under kotlinx.serialization +private val JsonElement.isJsonObject: Boolean get() = this is JsonObject +private val JsonElement.asJsonObject: JsonObject get() = this as JsonObject +private val JsonElement.isJsonArray: Boolean get() = this is JsonArray +private val JsonElement.asJsonArray: JsonArray get() = this as JsonArray +private val JsonElement.isJsonNull: Boolean get() = this is JsonNull +private val JsonElement.isJsonPrimitive: Boolean get() = this is JsonPrimitive +private val JsonElement.asString: String get() = (this as JsonPrimitive).content +private val JsonElement.asDouble: Double get() = (this as JsonPrimitive).double +private val JsonElement.asInt: Int get() = (this as JsonPrimitive).int +private val JsonElement.asBoolean: Boolean get() = (this as JsonPrimitive).boolean + +private fun JsonElement.has(key: String): Boolean = (this as? JsonObject)?.containsKey(key) == true + +private fun JsonObject.entrySet(): Set> = entries + +private fun JsonObject.keySet(): Set = keys + +private fun JsonArray.size(): Int = size diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index 44174da5..aaf403b9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -18,6 +18,7 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.HermesWsClient import com.m57.hermescontrol.data.ws.WsEvent import com.m57.hermescontrol.data.ws.WsMethods +import com.m57.hermescontrol.data.ws.toJsonElement import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.decodeFromJsonElement import java.util.concurrent.ConcurrentHashMap private const val TAG = "ChatViewModel" @@ -845,7 +847,9 @@ class ChatViewModel( val body = result.data val platformsStr = body.gateway_platforms?.entries?.joinToString("\n") { (k, v) -> - " • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}" + val status = v?.state ?: "Unknown" + val err = v?.error_code?.let { " (Error: $it)" } ?: "" + " • **$k**: $status$err" } ?: "No active platforms" addAssistantMessage( """ @@ -983,13 +987,15 @@ class ChatViewModel( fun refreshCurrentSession() { val sessionId = _uiState.value.currentSessionId ?: return - loadCachedMessages(sessionId) - // Skip the REST call if we already have messages — the WebSocket keeps - // the UI current. The REST endpoint can 404 on tab switch-back when the - // WS session ID doesn't round-trip through the resolver (issue #366), - // showing a misleading error banner while the connection is fine. - if (_uiState.value.messages.isNotEmpty()) return - loadSessionMessages(sessionId) + viewModelScope.launch { + loadCachedMessages(sessionId).join() + // Skip the REST call if we already have messages — the WebSocket keeps + // the UI current. The REST endpoint can 404 on tab switch-back when the + // WS session ID doesn't round-trip through the resolver (issue #366), + // showing a misleading error banner while the connection is fine. + if (_uiState.value.messages.isNotEmpty()) return@launch + loadSessionMessages(sessionId) + } } fun refreshSettings() { @@ -1004,8 +1010,8 @@ class ChatViewModel( @Suppress("UNCHECKED_CAST") private fun parseCommandCatalog(map: Map<*, *>): CommandCatalog? = try { - val json = OkHttpProvider.gson.toJson(map) - OkHttpProvider.gson.fromJson(json, CommandCatalog::class.java) + val jsonElement = map.toJsonElement() + OkHttpProvider.json.decodeFromJsonElement(jsonElement) } catch (e: Exception) { Log.e(TAG, "Failed to parse command catalog", e) null @@ -1030,23 +1036,25 @@ class ChatViewModel( ) } _streamingState.update { StreamingState() } - // Step 1: Load cached messages first — instant display - loadCachedMessages(sessionId) - // Step 2: Resume session on server - viewModelScope.launch(Dispatchers.IO) { - wsClient.send( - WsMethods.SESSION_RESUME, - mapOf("session_id" to sessionId), - onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) }, - ) + viewModelScope.launch { + // Step 1: Load cached messages first — instant display + loadCachedMessages(sessionId).join() + // Step 2: Resume session on server + launch(Dispatchers.IO) { + wsClient.send( + WsMethods.SESSION_RESUME, + mapOf("session_id" to sessionId), + onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) }, + ) + } + // Step 3: Load fresh messages from REST and merge + loadSessionMessages(sessionId) + // Step 4: Refresh session list to get latest titles + loadSessions() } - // Step 3: Load fresh messages from REST and merge - loadSessionMessages(sessionId) - // Step 4: Refresh session list to get latest titles - loadSessions() } - private fun loadCachedMessages(sessionId: String) { + private fun loadCachedMessages(sessionId: String): Job = viewModelScope.launch(Dispatchers.IO) { val cachedMessages = repo.loadMessages(sessionId) _uiState.update { state -> @@ -1058,7 +1066,6 @@ class ChatViewModel( } } } - } private fun loadSessionMessages(sessionId: String) { viewModelScope.launch { @@ -1082,7 +1089,7 @@ class ChatViewModel( // Room duplicates when re-loading the same session. val stableId = "rest-$sessionId-$index" // Preserve original timestamp from the API when available - val ts = msg.timestamp?.toLongOrNull() ?: System.currentTimeMillis() + val ts = msg.timestampText?.toLongOrNull() ?: System.currentTimeMillis() ChatMessage( id = stableId, role = role, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatWsEventReducer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatWsEventReducer.kt index a8fc1c47..30cfefdb 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatWsEventReducer.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatWsEventReducer.kt @@ -2,6 +2,8 @@ package com.m57.hermescontrol.ui.chat import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.data.ws.WsEvent +import com.m57.hermescontrol.data.ws.toJsonElement +import kotlinx.serialization.encodeToString /** * Pure state reducer for WebSocket events. @@ -225,7 +227,7 @@ object ChatWsEventReducer { ): ReducerResult { val contentJson = event.data?.let { - OkHttpProvider.gson.toJson(it) + OkHttpProvider.json.encodeToString(it.toJsonElement()) } ?: "" val toolMessage = ChatMessage( @@ -271,7 +273,7 @@ object ChatWsEventReducer { ): ReducerResult { val contentJson = event.data?.let { - OkHttpProvider.gson.toJson(it) + OkHttpProvider.json.encodeToString(it.toJsonElement()) } ?: "" val messages = state.messages.toMutableList() val toolIdx = diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt index 3e08a980..94c7e60b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt @@ -53,8 +53,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.gson.JsonElement -import com.google.gson.JsonPrimitive import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.SchemaField import com.m57.hermescontrol.ui.common.ErrorState @@ -63,6 +61,11 @@ import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.NavIcon import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.ToastEffect +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -580,7 +583,7 @@ private fun ConfigField( modifier = Modifier.weight(1f), ) Switch( - checked = currentValue?.asBoolean ?: false, + checked = (currentValue as? JsonPrimitive)?.booleanOrNull ?: false, onCheckedChange = { onChange(JsonPrimitive(it)) }, ) } @@ -590,7 +593,7 @@ private fun ConfigField( DropdownField( label = label, options = field.options ?: emptyList(), - selectedValue = currentValue?.asString ?: "", + selectedValue = (currentValue as? JsonPrimitive)?.content ?: "", onOptionSelected = { onChange(JsonPrimitive(it)) }, ) if (description != label) { @@ -607,7 +610,7 @@ private fun ConfigField( OutlinedTextField( value = currentValue?.let { - if (it.isJsonPrimitive) it.asString else "" + if (it is JsonPrimitive) it.content else "" } ?: "", onValueChange = { text -> text.toDoubleOrNull()?.let { doubleVal -> @@ -630,10 +633,10 @@ private fun ConfigField( // String or other type val textValue = currentValue?.let { - when { - it.isJsonPrimitive -> it.asString - it.isJsonObject -> it.toString() - it.isJsonArray -> it.toString() + when (it) { + is JsonPrimitive -> it.content + is JsonObject -> it.toString() + is JsonArray -> it.toString() else -> "" } } ?: "" @@ -708,20 +711,17 @@ private fun collectUncoveredPaths( val dotPath = if (prefix.isEmpty()) key else "$prefix.$key" // If this exact path is in the schema, it's covered — skip entirely if (dotPath in schemaPaths) continue - if (value is com.google.gson.JsonObject) { - // Recurse into nested objects to find uncovered leaves - val subKeys = value.keySet() + if (value is JsonObject) { // Before recursing deeper, check if this whole sub-tree has any schema coverage val hasSchemaCoverage = schemaPaths.any { it == dotPath || it.startsWith("$dotPath.") } if (hasSchemaCoverage) { // Schema covers some sub-paths — recurse to find what's NOT covered - val subMap = subKeys.associateWith { value.get(it) } - result.addAll(collectUncoveredPaths(subMap, schemaPaths, dotPath)) + result.addAll(collectUncoveredPaths(value, schemaPaths, dotPath)) } else { // No schema coverage at all for this subtree — add the whole thing result.add(dotPath) } - } else if (value is com.google.gson.JsonArray) { + } else if (value is JsonArray) { // Arrays: check if the path is in schema; if not, add it if (dotPath !in schemaPaths) { result.add(dotPath) @@ -746,7 +746,7 @@ private fun getJsonValue( if (i == 0) { current = config[key] } else { - current = current?.asJsonObject?.get(key) + current = (current as? JsonObject)?.get(key) } if (current == null) return null } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt index 0879e4a1..4a6e8193 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt @@ -2,8 +2,6 @@ package com.m57.hermescontrol.ui.config import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.gson.JsonElement -import com.google.gson.JsonObject import com.m57.hermescontrol.data.model.ConfigSchemaResponse import com.m57.hermescontrol.data.model.ConfigUpdateRequest import com.m57.hermescontrol.data.model.UpdateRawConfigRequest @@ -19,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject data class ConfigUiState( val isLoading: Boolean = false, @@ -296,15 +296,14 @@ class ConfigViewModel : val topKey = parts.first() val rest = parts.drop(1).joinToString(".") val topValue = config[topKey] - val nestedJson = topValue?.asJsonObject ?: JsonObject() + val nestedJson = (topValue as? JsonObject) ?: JsonObject(emptyMap()) val updatedNested = applyNestedValue( - nestedJson.entrySet().associate { it.key to it.value }, + nestedJson, rest, value, ) - val nestedObj = JsonObject() - updatedNested.forEach { (k, v) -> nestedObj.add(k, v) } + val nestedObj = JsonObject(updatedNested) val mutable = config.toMutableMap() mutable[topKey] = nestedObj return mutable @@ -312,23 +311,38 @@ class ConfigViewModel : /** Build a nested JSON changeset from dot-path pending changes. */ private fun buildChangeset(changes: Map): Map { - val root = JsonObject() + val root = mutableMapOf() for ((dotPath, value) in changes) { val parts = dotPath.split(".") var current = root for (i in 0 until parts.size - 1) { val key = parts[i] - val existing = current.get(key) - if (existing == null || !existing.isJsonObject) { - val newObj = JsonObject() - current.add(key, newObj) - current = newObj + val existing = current[key] + if (existing !is MutableMap<*, *>) { + val newMap = mutableMapOf() + current[key] = newMap + current = newMap } else { - current = existing.asJsonObject + @Suppress("UNCHECKED_CAST") + current = existing as MutableMap } } - current.add(parts.last(), value) + current[parts.last()] = value } - return root.entrySet().associate { it.key to it.value as JsonElement } + + fun toJsonObject(map: Map): JsonObject { + val content = + map.mapValues { (_, v) -> + if (v is Map<*, *>) { + @Suppress("UNCHECKED_CAST") + toJsonObject(v as Map) + } else { + v as JsonElement + } + } + return JsonObject(content) + } + + return toJsonObject(root) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt index dde2b9a8..5af7c65a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt @@ -9,6 +9,7 @@ import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkError import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -17,6 +18,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive data class ConnectUiState( val token: String = "", @@ -317,13 +320,10 @@ class ConnectViewModel( val decodedBytes = android.util.Base64.decode(trimmed, android.util.Base64.DEFAULT) val decodedString = String(decodedBytes, Charsets.UTF_8) if (decodedString.startsWith("{") && decodedString.endsWith("}")) { - val json = - com.google.gson.JsonParser - .parseString(decodedString) - .asJsonObject - val host = json.get("host")?.asString - val port = json.get("port")?.asInt?.toString() ?: json.get("port")?.asString - val token = json.get("token")?.asString + val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject + val host = json["host"]?.jsonPrimitive?.content + val port = json["port"]?.jsonPrimitive?.content + val token = json["token"]?.jsonPrimitive?.content if (host != null && port != null && token != null) { _uiState.update { it.copy( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt index 8e92928e..be2449a9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt @@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.model.UpdateCronJobRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.data.ws.toJsonElement import com.m57.hermescontrol.ui.common.ToastHost import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers @@ -18,6 +19,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive data class CronJobsUiState( val isLoading: Boolean = false, @@ -292,7 +295,7 @@ class CronJobsViewModel : safeApiCall { ApiClient.hermesApi.updateCronJob( editor.jobId ?: "", - UpdateCronJobRequest(updates = updates), + UpdateCronJobRequest(updates = updates.mapValues { it.value.toJsonElement() }), ) } } @@ -336,8 +339,8 @@ class CronJobsViewModel : private fun extractScheduleString(job: CronJob): String { val s = job.schedule return when (s) { - is String -> s - is Map<*, *> -> (s["value"] as? String) ?: job.scheduleText + is JsonPrimitive -> s.content + is JsonObject -> (s["value"] as? JsonPrimitive)?.content ?: job.scheduleText else -> job.scheduleText } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt index a3cfea12..5cb20a03 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt @@ -267,7 +267,7 @@ fun GatewayScreen( style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, ) - val stateText = pStatus.state?.uppercase() ?: "UNKNOWN" + val stateText = pStatus?.state?.uppercase() ?: "UNKNOWN" val badgeColor = when (stateText) { "RUNNING" -> Color(0xFF4CAF50) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingCodeEntryViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingCodeEntryViewModel.kt index 14fa9d0e..a30a882e 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingCodeEntryViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingCodeEntryViewModel.kt @@ -6,12 +6,12 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.google.gson.JsonParser import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkError import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive /** * UI state for the pairing code entry screen. @@ -88,12 +90,10 @@ class PairingCodeEntryViewModel( val decodedBytes = Base64.decode(trimmed, Base64.DEFAULT) val decodedString = String(decodedBytes, Charsets.UTF_8) if (decodedString.startsWith("{") && decodedString.endsWith("}")) { - val json = JsonParser.parseString(decodedString).asJsonObject - val host = json.get("host")?.asString - val port = - json.get("port")?.asInt?.toString() - ?: json.get("port")?.asString - val token = json.get("token")?.asString + val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject + val host = json["host"]?.jsonPrimitive?.content + val port = json["port"]?.jsonPrimitive?.content + val token = json["token"]?.jsonPrimitive?.content // Accept raw "host" + "port" + "token", or a composite "pairing" payload if (host != null && port != null && token != null) { connectToDashboard(host, port.toIntOrNull() ?: 9119, token) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt index 6d382bdf..4594a8fd 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt @@ -786,7 +786,7 @@ private fun LazyListScope.gatewaySection( style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) - platform.state?.let { platformState -> + platform?.state?.let { platformState -> StatusBadge( text = platformState, status = diff --git a/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt b/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt index 819856e6..4690c4b6 100644 --- a/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt @@ -61,6 +61,7 @@ import io.mockk.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.* +import kotlinx.serialization.json.JsonPrimitive import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.After import org.junit.Assert.* @@ -332,7 +333,10 @@ class E2eIntegrationTest { advanceUntilIdle() assertFalse(viewModel.uiState.value.isHubSearching) - assertTrue(viewModel.uiState.value.hubResults.isEmpty()) + assertTrue( + viewModel.uiState.value.hubResults + .isEmpty(), + ) assertNull(viewModel.uiState.value.hubSearchError) } @@ -349,7 +353,8 @@ class E2eIntegrationTest { assertFalse(viewModel.uiState.value.isHubSearching) assertNotNull(viewModel.uiState.value.hubSearchError) assertTrue( - viewModel.uiState.value.hubSearchError!!.contains("HTTP 500"), + viewModel.uiState.value.hubSearchError!! + .contains("HTTP 500"), ) } @@ -360,7 +365,10 @@ class E2eIntegrationTest { viewModel.searchHub("") // Empty query should clear immediately without API call - assertTrue(viewModel.uiState.value.hubResults.isEmpty()) + assertTrue( + viewModel.uiState.value.hubResults + .isEmpty(), + ) assertFalse(viewModel.uiState.value.isHubSearching) assertNull(viewModel.uiState.value.hubSearchError) } @@ -384,7 +392,8 @@ class E2eIntegrationTest { assertNull(viewModel.uiState.value.installingSkillName) assertNotNull(viewModel.uiState.value.toastMessage) assertTrue( - viewModel.uiState.value.toastMessage!!.contains("Installed"), + viewModel.uiState.value.toastMessage!! + .contains("Installed"), ) // Refresh was called assertFalse(viewModel.uiState.value.isLoading) @@ -405,7 +414,8 @@ class E2eIntegrationTest { assertNull(viewModel.uiState.value.installingSkillName) assertNotNull(viewModel.uiState.value.toastMessage) assertTrue( - viewModel.uiState.value.toastMessage!!.contains("HTTP 500"), + viewModel.uiState.value.toastMessage!! + .contains("HTTP 500"), ) } @@ -425,7 +435,8 @@ class E2eIntegrationTest { assertNull(viewModel.uiState.value.uninstallingSkillName) assertNotNull(viewModel.uiState.value.toastMessage) assertTrue( - viewModel.uiState.value.toastMessage!!.contains("Uninstalled"), + viewModel.uiState.value.toastMessage!! + .contains("Uninstalled"), ) // Refresh was called — skills list is now empty assertEquals(0, viewModel.uiState.value.skills.size) @@ -445,7 +456,8 @@ class E2eIntegrationTest { assertNull(viewModel.uiState.value.uninstallingSkillName) assertNotNull(viewModel.uiState.value.toastMessage) assertTrue( - viewModel.uiState.value.toastMessage!!.contains("HTTP 500"), + viewModel.uiState.value.toastMessage!! + .contains("HTTP 500"), ) } @@ -487,7 +499,8 @@ class E2eIntegrationTest { assertNull(viewModel.uiState.value.previewSkillContent) assertNotNull(viewModel.uiState.value.toastMessage) assertTrue( - viewModel.uiState.value.toastMessage!!.contains("HTTP 500"), + viewModel.uiState.value.toastMessage!! + .contains("HTTP 500"), ) } @@ -495,7 +508,7 @@ class E2eIntegrationTest { @Test fun testCronJobsListing_success() = runTest { - val job = CronJob("id1", "Job 1", "*/5 * * * *", "active", "success", "2026-06-15T15:10:00Z") + val job = CronJob("id1", "Job 1", JsonPrimitive("*/5 * * * *"), "active", "success", "2026-06-15T15:10:00Z") coEvery { mockApiService.getCronJobs() } returns Response.success(listOf(job)) val viewModel = CronJobsViewModel() @@ -536,7 +549,7 @@ class E2eIntegrationTest { @Test fun testCronJobPause_success() = runTest { - val job = CronJob("id1", "Job 1", "*/5 * * * *", "active", null, null) + val job = CronJob("id1", "Job 1", JsonPrimitive("*/5 * * * *"), "active", null, null) coEvery { mockApiService.getCronJobs() } returns Response.success(listOf(job)) coEvery { mockApiService.pauseCronJob("id1") } returns Response.success(Unit) @@ -563,7 +576,7 @@ class E2eIntegrationTest { @Test fun testCronJobResume_success() = runTest { - val job = CronJob("id1", "Job 1", "*/5 * * * *", "paused", null, null) + val job = CronJob("id1", "Job 1", JsonPrimitive("*/5 * * * *"), "paused", null, null) coEvery { mockApiService.getCronJobs() } returns Response.success(listOf(job)) coEvery { mockApiService.resumeCronJob("id1") } returns Response.success(Unit) diff --git a/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt b/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt index f278ee47..54485518 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt @@ -1,19 +1,40 @@ package com.m57.hermescontrol.data.model -import com.google.gson.Gson -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test +import com.m57.hermescontrol.data.remote.OkHttpProvider +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test class ModelSerializationTest { - private val gson = Gson() + private val json = OkHttpProvider.json @Test - fun testSkillDeserialization_allFields() { - val json = + fun testSkillSerialization() { + val skill = + Skill( + name = "weather", + description = "Get current weather info", + category = "utility", + enabled = true, + ) + val jsonStr = json.encodeToString(skill) + val deserialized = json.decodeFromString(jsonStr) + assertEquals(skill.name, deserialized.name) + assertEquals(skill.description, deserialized.description) + assertEquals(skill.category, deserialized.category) + assertEquals(skill.enabled, deserialized.enabled) + } + + @Test + fun testSkillDeserialization() { + val jsonStr = """ { "name": "weather", @@ -22,7 +43,7 @@ class ModelSerializationTest { "enabled": true } """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) + val skill = json.decodeFromString(jsonStr) assertEquals("weather", skill.name) assertEquals("Get current weather info", skill.description) assertEquals("utility", skill.category) @@ -30,170 +51,145 @@ class ModelSerializationTest { } @Test - fun testSkillDeserialization_optionalFieldsNull() { - val json = - """ - { - "name": "light_control", - "description": null, - "category": null, - "enabled": false - } - """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) - assertEquals("light_control", skill.name) - assertNull(skill.description) - assertNull(skill.category) - assertEquals(false, skill.enabled) + fun testCronJobSerialization() { + val job = + CronJob( + id = "job-123", + name = "Daily Backup", + schedule = JsonPrimitive("0 0 * * *"), + state = "active", + last_run_status = null, + next_run = null, + ) + val jsonStr = json.encodeToString(job) + val deserialized = json.decodeFromString(jsonStr) + assertEquals(job.id, deserialized.id) + assertEquals(job.name, deserialized.name) + assertEquals(job.schedule, deserialized.schedule) + assertEquals(job.state, deserialized.state) } @Test - fun testCronJobDeserialization_allFields() { - val json = + fun testCronJobDeserialization() { + val jsonStr = """ { "id": "job-123", "name": "Daily Backup", "schedule": "0 0 * * *", - "state": "active", - "last_run_status": "success", - "next_run": "2026-06-16T00:00:00Z" + "state": "active" } """.trimIndent() - val job = gson.fromJson(json, CronJob::class.java) + val job = json.decodeFromString(jsonStr) assertEquals("job-123", job.id) assertEquals("Daily Backup", job.name) - assertEquals("0 0 * * *", job.schedule) + assertEquals("0 0 * * *", job.scheduleText) assertEquals("active", job.state) - assertEquals("success", job.last_run_status) - assertEquals("2026-06-16T00:00:00Z", job.next_run) - } - - @Test - fun testCronJobDeserialization_optionalFieldsNull() { - val json = - """ - { - "id": "job-456", - "name": "One-off Task", - "schedule": null, - "state": null, - "last_run_status": null, - "next_run": null - } - """.trimIndent() - val job = gson.fromJson(json, CronJob::class.java) - assertEquals("job-456", job.id) - assertEquals("One-off Task", job.name) - assertNull(job.schedule) - assertNull(job.state) - assertNull(job.last_run_status) - assertNull(job.next_run) } @Test fun testToggleSkillRequestSerialization() { val request = ToggleSkillRequest(name = "weather", enabled = false) - val json = gson.toJson(request) + val jsonStr = json.encodeToString(request) // Ensure serialization produces correct JSON structure - val parsed = gson.fromJson(json, Map::class.java) - assertEquals("weather", parsed["name"]) - assertEquals(false, parsed["enabled"]) + val parsed = json.decodeFromString>(jsonStr) + assertEquals("weather", (parsed["name"] as? JsonPrimitive)?.content) + assertEquals(false, (parsed["enabled"] as? JsonPrimitive)?.booleanOrNull) } @Test fun testSkillDeserialization_missingRequiredFields() { - val json = "{}" - val skill = gson.fromJson(json, Skill::class.java) - // Verify null-safety issue: Gson allows non-nullable 'name' to be null at runtime - assertNull(skill.name) - // Primitive boolean defaults to false in Gson/JVM when missing - assertEquals(false, skill.enabled) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSkillDeserialization_explicitNullForNonNullable() { - val json = + val jsonStr = """ { "name": null, "enabled": null } """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) - assertNull(skill.name) - assertEquals(false, skill.enabled) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testCronJobDeserialization_missingRequiredFields() { - val json = "{}" - val job = gson.fromJson(json, CronJob::class.java) - assertNull(job.id) - assertNull(job.name) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionListResponseDeserialization_missingRequiredFields() { - val json = "{}" - val response = gson.fromJson(json, SessionListResponse::class.java) - assertNull(response.sessions) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionInfoDeserialization_missingRequiredFields() { - val json = "{}" - val info = gson.fromJson(json, SessionInfo::class.java) - assertNull(info.id) - assertNull(info.title) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionMessagesResponseDeserialization_missingRequiredFields() { - val json = "{}" - val response = gson.fromJson(json, SessionMessagesResponse::class.java) - assertNull(response.messages) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSkillDeserialization_malformedJson() { - val json = "{\"name\": \"weather\"," - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { - gson.fromJson(json, Skill::class.java) + val jsonStr = "{\"name\": \"weather\"," + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) } } @Test fun testSkillDeserialization_typeMismatchObject() { - val json = + val jsonStr = """ { "name": "weather", "enabled": {} } """.trimIndent() - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { - gson.fromJson(json, Skill::class.java) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) } } @Test fun testSkillDeserialization_typeMismatchArray() { - val json = + val jsonStr = """ { "name": "weather", "enabled": [] } """.trimIndent() - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { - gson.fromJson(json, Skill::class.java) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) } } @Test fun testSkillDeserialization_missingName_causesNullOnNonNullableField() { - val json = + val jsonStr = """ { "description": "Get current weather info", @@ -201,15 +197,14 @@ class ModelSerializationTest { "enabled": true } """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) - // Gson bypasses Kotlin null-safety constraints, resulting in null on non-nullable field - val nameNullable: String? = skill.name - assertNull(nameNullable) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSkillDeserialization_nullName_causesNullOnNonNullableField() { - val json = + val jsonStr = """ { "name": null, @@ -218,14 +213,14 @@ class ModelSerializationTest { "enabled": true } """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) - val nameNullable: String? = skill.name - assertNull(nameNullable) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSkillDeserialization_missingEnabled_usesJvmDefaultValue() { - val json = + val jsonStr = """ { "name": "weather", @@ -233,14 +228,14 @@ class ModelSerializationTest { "category": "utility" } """.trimIndent() - val skill = gson.fromJson(json, Skill::class.java) - // Boolean primitive defaults to false - assertEquals(false, skill.enabled) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testCronJobDeserialization_missingId_causesNullOnNonNullableField() { - val json = + val jsonStr = """ { "name": "Daily Backup", @@ -248,52 +243,52 @@ class ModelSerializationTest { "state": "active" } """.trimIndent() - val job = gson.fromJson(json, CronJob::class.java) - val idNullable: String? = job.id - assertNull(idNullable) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionListResponseDeserialization_missingSessions_causesNullOnNonNullableField() { - val json = "{}" - val response = gson.fromJson(json, SessionListResponse::class.java) - val sessionsNullable: List? = response.sessions - assertNull(sessionsNullable) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionInfoDeserialization_missingId_causesNullOnNonNullableField() { - val json = + val jsonStr = """ { "title": "My Session", "created_at": "2026-06-15T12:00:00Z" } """.trimIndent() - val info = gson.fromJson(json, SessionInfo::class.java) - val idNullable: String? = info.id - assertNull(idNullable) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testSessionMessagesResponseDeserialization_missingMessages_causesNullOnNonNullableField() { - val json = "{}" - val response = gson.fromJson(json, SessionMessagesResponse::class.java) - val messagesNullable: List? = response.messages - assertNull(messagesNullable) + val jsonStr = "{}" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testDeserialization_malformedJson_throwsJsonSyntaxException() { - val json = """{"name": "weather", "enabled": """ - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { - gson.fromJson(json, Skill::class.java) + val jsonStr = """{"name": "weather", "enabled": """ + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) } } @Test fun testStatusResponseDeserialization_allFields() { - val json = + val jsonStr = """ { "version": "1.0.0", @@ -312,7 +307,7 @@ class ModelSerializationTest { } } """.trimIndent() - val response = gson.fromJson(json, StatusResponse::class.java) + val response = json.decodeFromString(jsonStr) assertEquals("1.0.0", response.version) assertEquals(true, response.gateway_running) assertEquals(5, response.active_sessions) @@ -326,8 +321,8 @@ class ModelSerializationTest { @Test fun testStatusResponseDeserialization_missingFields() { - val json = "{}" - val response = gson.fromJson(json, StatusResponse::class.java) + val jsonStr = "{}" + val response = json.decodeFromString(jsonStr) assertNull(response.version) assertNull(response.gateway_running) assertNull(response.active_sessions) @@ -337,7 +332,7 @@ class ModelSerializationTest { @Test fun testStatusResponseDeserialization_gatewayPlatformsNullValue() { - val json = + val jsonStr = """ { "gateway_platforms": { @@ -348,7 +343,7 @@ class ModelSerializationTest { } } """.trimIndent() - val response = gson.fromJson(json, StatusResponse::class.java) + val response = json.decodeFromString(jsonStr) val platforms = response.gateway_platforms assertNotNull(platforms) assertNull(platforms!!["android"]) @@ -360,20 +355,20 @@ class ModelSerializationTest { @Test fun testStatusResponseDeserialization_typeMismatchInGatewayPlatforms() { - val json = + val jsonStr = """ { "gateway_platforms": "not_a_map" } """.trimIndent() - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { - gson.fromJson(json, StatusResponse::class.java) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) } } @Test fun testSystemStatsResponse_deserialization() { - val json = + val jsonStr = """ { "os": "Linux", @@ -402,7 +397,7 @@ class ModelSerializationTest { } } """.trimIndent() - val response = gson.fromJson(json, SystemStatsResponse::class.java) + val response = json.decodeFromString(jsonStr) assertEquals("Linux", response.os) assertEquals("aarch64", response.arch) assertEquals("hermes-box", response.hostname) @@ -413,36 +408,31 @@ class ModelSerializationTest { assertEquals(1.5, response.load_avg?.get(0) ?: 0.0, 0.01) assertEquals(86400.0, response.uptime_seconds ?: 0.0, 0.01) assertEquals(50.0, response.memory?.percent ?: 0.0, 0.01) - assertEquals(8388608, response.memory?.total) + assertEquals(8388608L, response.memory?.total) assertEquals(50.0, response.disk?.percent ?: 0.0, 0.01) } @Test fun testToggleSkillRequestDeserialization_missingRequiredFields() { - val json = """{"enabled":true}""" - val request = gson.fromJson(json, ToggleSkillRequest::class.java) - val nameNullable: String? = request.name - assertNull(nameNullable) - assertEquals(true, request.enabled) + val jsonStr = """{"enabled":true}""" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testToggleSkillRequestDeserialization_explicitNull() { - val json = """{"name":null,"enabled":null}""" - val request = gson.fromJson(json, ToggleSkillRequest::class.java) - val nameNullable: String? = request.name - assertNull(nameNullable) - assertEquals(false, request.enabled) + val jsonStr = """{"name":null,"enabled":null}""" + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonStr) + } } @Test fun testToggleSkillRequestSerialization_withNullName() { val jsonInput = """{"name":null,"enabled":true}""" - val request = gson.fromJson(jsonInput, ToggleSkillRequest::class.java) - - val jsonOutput = gson.toJson(request) - val parsed = gson.fromJson(jsonOutput, Map::class.java) - assertFalse(parsed.containsKey("name")) - assertEquals(true, parsed["enabled"]) + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { + json.decodeFromString(jsonInput) + } } } diff --git a/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceMockWebServerTest.kt b/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceMockWebServerTest.kt index 518782b6..d91fdd51 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceMockWebServerTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceMockWebServerTest.kt @@ -3,6 +3,7 @@ package com.m57.hermescontrol.data.remote import com.m57.hermescontrol.data.model.SessionMessage import com.m57.hermescontrol.data.model.StatusResponse import kotlinx.coroutines.runBlocking +import okhttp3.MediaType.Companion.toMediaType import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After @@ -13,7 +14,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.kotlinx.serialization.asConverterFactory /** * MockWebServer-based tests for [HermesApiService]. @@ -35,7 +36,7 @@ class HermesApiServiceMockWebServerTest { Retrofit .Builder() .baseUrl(mockServer.url("/")) - .addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson)) + .addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType())) .build() .create(HermesApiService::class.java) } @@ -244,7 +245,7 @@ class HermesApiServiceMockWebServerTest { assertEquals("user", messages[0].role) assertEquals("Hello Hermes", messages[0].content) - assertEquals("1718000000", messages[0].timestamp) + assertEquals("1718000000", messages[0].timestampText) assertEquals("assistant", messages[1].role) assertEquals("Hi! How can I help?", messages[1].content) diff --git a/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceTest.kt b/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceTest.kt index 06689fcb..4d472380 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/remote/HermesApiServiceTest.kt @@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.remote import com.m57.hermescontrol.data.model.ToggleSkillRequest import kotlinx.coroutines.test.runTest +import okhttp3.MediaType.Companion.toMediaType import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach @@ -13,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.kotlinx.serialization.asConverterFactory class HermesApiServiceTest { private lateinit var mockWebServer: MockWebServer @@ -28,7 +29,7 @@ class HermesApiServiceTest { Retrofit .Builder() .baseUrl(mockWebServer.url("/")) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType())) .build() apiService = retrofit.create(HermesApiService::class.java) @@ -151,58 +152,49 @@ class HermesApiServiceTest { } @Test - fun testGetSkills_missingFieldsInResponse() = - runTest { - mockWebServer.enqueue( - MockResponse() - .setResponseCode(200) - .setBody("""[{"description":"info"}]"""), - ) + fun testGetSkills_missingFieldsInResponse() { + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody("""[{"description":"info"}]"""), + ) - val response = apiService.getSkills() - assertTrue(response.isSuccessful) - val skills = response.body() - assertEquals(1, skills?.size) - val skill = skills?.get(0) - org.junit.jupiter.api.Assertions - .assertNull(skill?.name) - assertEquals(false, skill?.enabled) + assertThrows(kotlinx.serialization.SerializationException::class.java) { + runTest { + apiService.getSkills() + } } + } @Test - fun testGetCronJobs_missingFieldsInResponse() = - runTest { - mockWebServer.enqueue( - MockResponse() - .setResponseCode(200) - .setBody("""[{"schedule":"* * * * *"}]"""), - ) + fun testGetCronJobs_missingFieldsInResponse() { + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody("""[{"schedule":"* * * * *"}]"""), + ) - val response = apiService.getCronJobs() - assertTrue(response.isSuccessful) - val jobs = response.body() - assertEquals(1, jobs?.size) - val job = jobs?.get(0) - org.junit.jupiter.api.Assertions - .assertNull(job?.id) - org.junit.jupiter.api.Assertions - .assertNull(job?.name) + assertThrows(kotlinx.serialization.SerializationException::class.java) { + runTest { + apiService.getCronJobs() + } } + } @Test - fun testGetSessions_missingSessionsField() = - runTest { - mockWebServer.enqueue( - MockResponse() - .setResponseCode(200) - .setBody("""{}"""), - ) + fun testGetSessions_missingSessionsField() { + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody("""{}"""), + ) - val response = apiService.getSessions() - assertTrue(response.isSuccessful) - org.junit.jupiter.api.Assertions - .assertNull(response.body()?.sessions) + assertThrows(kotlinx.serialization.SerializationException::class.java) { + runTest { + apiService.getSessions() + } } + } @Test fun testGetSessionMessages_apiReturnsMalformedJson_throwsException() { @@ -212,7 +204,7 @@ class HermesApiServiceTest { .setBody("""{"messages": ["""), ) - assertThrows(java.io.EOFException::class.java) { + assertThrows(kotlinx.serialization.SerializationException::class.java) { runTest { apiService.getSessionMessages("session_1") } @@ -220,19 +212,19 @@ class HermesApiServiceTest { } @Test - fun testGetSessionMessages_missingMessagesField() = - runTest { - mockWebServer.enqueue( - MockResponse() - .setResponseCode(200) - .setBody("""{}"""), - ) + fun testGetSessionMessages_missingMessagesField() { + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody("""{}"""), + ) - val response = apiService.getSessionMessages("session_1") - assertTrue(response.isSuccessful) - org.junit.jupiter.api.Assertions - .assertNull(response.body()?.messages) + assertThrows(kotlinx.serialization.SerializationException::class.java) { + runTest { + apiService.getSessionMessages("session_1") + } } + } @Test fun testGetSkills_malformedJsonResponse_throwsException() = @@ -243,7 +235,7 @@ class HermesApiServiceTest { .setBody("""[{"name":"weather""""), ) - org.junit.jupiter.api.Assertions.assertThrows(java.io.EOFException::class.java) { + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { kotlinx.coroutines.runBlocking { apiService.getSkills() } @@ -259,7 +251,7 @@ class HermesApiServiceTest { .setBody("""[{"name":"weather","enabled":{}}]"""), ) - org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) { + org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) { kotlinx.coroutines.runBlocking { apiService.getSkills() } diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt index 92d40309..bab85ee6 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt @@ -3,12 +3,31 @@ package com.m57.hermescontrol.data.ws import io.mockk.every import io.mockk.mockkStatic import io.mockk.unmockkAll +import kotlinx.serialization.json.JsonObject import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +@Suppress("FunctionName") +private fun createJsonRpcResponse( + jsonrpc: String, + id: String?, + result: Any? = null, + error: JsonRpcError? = null, + method: String? = null, + params: Any? = null, +): JsonRpcResponse = + JsonRpcResponse( + jsonrpc = jsonrpc, + id = id, + result = result?.toJsonElement(), + error = error, + method = method, + params = params?.toJsonElement() as? JsonObject, + ) + class EventParserTest { @Before fun setUp() { @@ -24,7 +43,7 @@ class EventParserTest { @Test fun testParseRpcResult_returnsRpcResultEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = "123", result = mapOf("status" to "success"), @@ -43,7 +62,7 @@ class EventParserTest { fun testParseRpcError_returnsRpcErrorEvent() { val error = JsonRpcError(code = -32600, message = "Invalid Request") val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = "456", result = null, @@ -62,7 +81,7 @@ class EventParserTest { @Test fun testParseGatewayReady_returnsGatewayReadyEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -83,7 +102,7 @@ class EventParserTest { @Test fun testParseMessageToken_returnsMessageTokenEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -105,7 +124,7 @@ class EventParserTest { @Test fun testParseMessageToken_withSessionIdInParams_returnsMessageTokenEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -128,7 +147,7 @@ class EventParserTest { @Test fun testParseThinkingDelta_returnsThinkingDeltaEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -150,7 +169,7 @@ class EventParserTest { @Test fun testParseClarifyRequest_returnsClarifyRequestEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -176,7 +195,7 @@ class EventParserTest { @Test fun testParseClarifyRequest_withQuestionFields_parsesSuccessfully() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -204,7 +223,7 @@ class EventParserTest { @Test fun testParseSessionInfo_returnsSessionInfoEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -220,7 +239,7 @@ class EventParserTest { @Test fun testParseMessageStart_returnsMessageStartEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -241,7 +260,7 @@ class EventParserTest { @Test fun testParseMessageDelta_returnsMessageTokenEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -258,7 +277,7 @@ class EventParserTest { @Test fun testParseMessageComplete_returnsMessageCompleteEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -274,7 +293,7 @@ class EventParserTest { @Test fun testParseMessageDone_returnsMessageDoneEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -289,7 +308,7 @@ class EventParserTest { @Test fun testParseToolStart_returnsToolStartEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -311,7 +330,7 @@ class EventParserTest { @Test fun testParseToolComplete_returnsToolCompleteEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -333,7 +352,7 @@ class EventParserTest { @Test fun testParseStatusUpdate_returnsStatusUpdateEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -349,7 +368,7 @@ class EventParserTest { @Test fun testParseSessionUpdated_returnsSessionUpdatedEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -365,7 +384,7 @@ class EventParserTest { @Test fun testParseUnknownType_returnsUnknownEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -380,7 +399,7 @@ class EventParserTest { @Test fun testParseNullParams_returnsUnknownEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, @@ -395,7 +414,7 @@ class EventParserTest { @Test fun testParseNullTypeInParams_returnsUnknownEvent() { val response = - JsonRpcResponse( + createJsonRpcResponse( jsonrpc = "2.0", id = null, result = null, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ddfb4ae6..d3e51426 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,6 @@ lifecycleViewmodelNav3 = "2.10.0" leakcanary = "2.14" okhttp = "5.4.0" retrofit = "3.0.0" -gson = "2.14.0" securityCrypto = "1.0.0" mockk = "1.14.11" turbine = "1.2.0" @@ -66,8 +65,8 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } -retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +retrofit-converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.0" } # Security androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }