Use kotlinx.serialization for pseuCo.com WebSpcket protocol

parent fd353f40
Pipeline #7545 passed with stages
in 6 minutes and 34 seconds
package com.pseuco
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlinx.serialization.internal.CommonEnumSerializer
......@@ -26,19 +25,16 @@ data class PseuCoComFile(val name: String, val content: String, @Required val ty
/**
* A pseuCo file.
*/
@SerializedName("pseuco")
PSEUCO,
/**
* A CCS file.
*/
@SerializedName("ccs")
CCS,
/**
* A LTS file.
*/
@SerializedName("lts")
LTS;
/**
......
@file:Suppress("EXPERIMENTAL_API_USAGE")
package com.pseuco.websocket
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import com.pseuco.PseuCoComFile
import com.pseuco.PseuCoServer
import com.pseuco.websocket.SocketMessage.Response.Error.Code
......@@ -27,6 +25,9 @@ import io.ktor.websocket.WebSocketServerSession
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElementTypeMismatchException
import kotlinx.serialization.json.JsonException
import util.Workspace
import java.io.File
import java.util.Random
......@@ -61,14 +62,6 @@ object PseuCoWebSocket : PseuCoServer {
*/
private val files = mutableMapOf<Int, PseuCoComFile>()
/**
* A [Gson] instance used by this server.
*
* @author Konstantin Kopper
* @since 2.0.0
*/
private val jsonBuilder = Gson()
/**
* Action triggered when receiving [SocketMessage.Request.Open] messages. Takes the newly created file as parameter.
* Can be set to update GUI accordingly.
......@@ -107,29 +100,28 @@ object PseuCoWebSocket : PseuCoServer {
return@webSocket
}
val request = JsonParser().parse(frame.readText()).let {
if (!it.isJsonObject) {
@Suppress("MoveVariableDeclarationIntoWhen")
val request = Json.plain.parseJson(frame.readText()).let {
val o = try {
it.jsonObject
} catch (e: JsonElementTypeMismatchException) {
sendError("Expected a JSON object.")
return@let null
}
if ("type" !in it.asJsonObject) {
if ("type" !in o) {
sendError("Missing type attribute.")
return@let null
}
try {
jsonBuilder.fromJson(it, when (it.asJsonObject["type"]!!.asString) {
"get" -> SocketMessage.Request.Get::class.java
"open" -> SocketMessage.Request.Open::class.java
else -> {
sendError("Unsupported request type '${it.asJsonObject["type"].asString}'.")
return@let null
}
})
} catch (e: JsonParseException) {
Json.nonstrict.fromJson(SocketMessage.serializer(), it)
} catch (e: JsonException) {
sendError("Parsing the request failed.")
null
} catch (e: Throwable) {
sendError("Internal server error: ${e.localizedMessage}")
null
}
} ?: continue@serverLoop
......@@ -191,7 +183,7 @@ object PseuCoWebSocket : PseuCoServer {
* @author Konstantin Kopper
* @since 2.0.0
*/
private fun createFrame(msg: SocketMessage): Frame = Frame.Text(jsonBuilder.toJson(msg))
private fun createFrame(msg: SocketMessage): Frame = Frame.Text(Json.stringify(SocketMessage.serializer(), msg))
/**
* Send [msg] over a WebSockets connection.
......@@ -220,15 +212,6 @@ object PseuCoWebSocket : PseuCoServer {
*/
private suspend fun WebSocketSession.sendError(code: Code, msg: String) = send(SocketMessage.Response.Error(msg, code))
/**
* Checks if a [JsonObject] contains a given attribute.
*
* @author Konstantin Kopper
* @since 2.0.0
* @param key The name of the attribute.
*/
private operator fun JsonObject.contains(key: String) = this.has(key)
/* Custom features */
/**
......
package com.pseuco.websocket
import com.google.gson.annotations.SerializedName
import com.pseuco.PseuCoComFile
import kotlinx.serialization.CompositeDecoder
import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.internal.CommonEnumSerializer
import kotlinx.serialization.internal.SerialClassDescImpl
/**
* All valid messages used in the pseuCo WebSockets file sharing protocol.
......@@ -13,6 +21,7 @@ import com.pseuco.PseuCoComFile
* @param type A string representation of the message type.
* @constructor Creates a new Socket message.
*/
@Serializable(with = SocketMessage.Companion::class)
sealed class SocketMessage(val type: String) {
/**
......@@ -101,7 +110,6 @@ sealed class SocketMessage(val type: String) {
* @author Konstantin Kopper
* @since 2.0.0
*/
@SerializedName("general")
GENERAL,
/**
......@@ -110,9 +118,87 @@ sealed class SocketMessage(val type: String) {
* @author Konstantin Kopper
* @since 2.0.0
*/
@SerializedName("fileNotFound")
FILE_NOT_FOUND,
FILE_NOT_FOUND;
/**
* Serializer for error [Code]s. Converts enum values to camel case strings.
*
* @author Konstantin Kopper
* @since 2.0.4
*/
companion object : CommonEnumSerializer<Code>("SocketMessage.Response.Error.Code", values(),
values().map { it.name.toLowerCase().replace(Regex("_(\\w)")) { match -> match.groupValues[1].toUpperCase() } }.toTypedArray())
}
}
}
/**
* Serializer for [SocketMessage]s.
*
* @author Konstantin Kopper
* @since 2.0.4
*/
companion object : KSerializer<SocketMessage> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("SocketMessage") {
init {
addElement("type")
addElement("id", isOptional = true)
addElement("file", isOptional = true)
addElement("message", isOptional = true)
addElement("code", isOptional = true)
}
}
override fun deserialize(decoder: Decoder): SocketMessage {
val structure = decoder.beginStructure(descriptor)
lateinit var type: String
var id: Int? = null
lateinit var file: PseuCoComFile
lateinit var message: String
lateinit var code: Response.Error.Code
tailrec fun decodeAttributes(): SocketMessage {
when (val i = structure.decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> {
structure.endStructure(descriptor)
return when (type) {
"get" -> Request.Get(id!!)
"open" -> Request.Open(file)
"file" -> Response.File(file)
"success" -> Response.Success
"error" -> Response.Error(message, code)
else -> throw SerializationException("Unknown message type: '$type'.")
}
}
0 -> type = structure.decodeStringElement(descriptor, i)
1 -> id = structure.decodeIntElement(descriptor, i)
2 -> file = structure.decodeSerializableElement(descriptor, i, PseuCoComFile.serializer())
3 -> message = structure.decodeStringElement(descriptor, i)
4 -> code = structure.decodeSerializableElement(descriptor, i, Response.Error.Code.Companion)
}
return decodeAttributes()
}
return decodeAttributes()
}
override fun serialize(encoder: Encoder, obj: SocketMessage) {
val structure = encoder.beginStructure(descriptor)
structure.encodeStringElement(descriptor, 0, obj.type)
when (obj) {
is Request.Get -> structure.encodeIntElement(descriptor, 1, obj.id)
is Request.Open -> structure.encodeSerializableElement(descriptor, 2, PseuCoComFile.serializer(), obj.file)
is Response.File -> structure.encodeSerializableElement(descriptor, 2, PseuCoComFile.serializer(), obj.file)
is Response.Success -> {
}
is Response.Error -> {
structure.encodeStringElement(descriptor, 3, obj.message)
structure.encodeSerializableElement(descriptor, 4, Response.Error.Code.Companion, obj.code)
}
}
structure.endStructure(descriptor)
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment