refactor: migrate Gson serialization to kotlinx.serialization (#475)
* refactor: migrate Gson serialization to kotlinx.serialization * fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement * test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults * fix(chat): support flexible numeric and string decoding for message timestamps * fix(chat): sequentialize cache loading to avoid overwriting network state
This commit is contained in:
64 files changed
+863
-577
No files matched your search
@@ -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<List<String>> = emptyList(),
|
||||
val sub: Map<String, List<String>> = 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<List<String>>,
|
||||
|
||||
@@ -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<String, Any?> ?: return WsEvent.Unknown(rawJson)
|
||||
val eventType = params["type"] as? String ?: return WsEvent.Unknown(rawJson)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
@@ -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<JsonRpcResponse>(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()) {
|
||||
|
||||
@@ -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<String, Any> = emptyMap(),
|
||||
val params: Map<String, JsonElement> = 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<String, Any?>?,
|
||||
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() }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user