All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.org.hildan.har.HarWebSocketModel.kt Maven / Gradle / Ivy

package org.hildan.har

import kotlinx.datetime.*
import kotlinx.io.bytestring.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import kotlin.io.encoding.*

/**
 * A web socket message record.
 */
@Serializable(with = WebSocketMessageSerializer::class)
sealed class HarWebSocketMessage {
    /** Which way the message was sent on the web socket. */
    abstract val direction: WebSocketDirection

    /** The instant when the message was sent. */
    abstract val time: Instant

    /** The opcode of the websocket frame (1 for a text frame, 2 for a binary frame). */
    abstract val opcode: Int

    @Serializable
    data class Text(
        @SerialName("type")
        override val direction: WebSocketDirection,
        @Serializable(with = InstantAsDoubleSecondsSerializer::class)
        override val time: Instant,
        override val opcode: Int,
        /** The text payload of this message (UTF-8 decoded). */
        val data: String,
    ) : HarWebSocketMessage()

    @Serializable
    data class Binary(
        @SerialName("type")
        override val direction: WebSocketDirection,
        @Serializable(with = InstantAsDoubleSecondsSerializer::class)
        override val time: Instant,
        override val opcode: Int,
        /** The binary payload of this message. */
        @Serializable(with = ByteStringAsBase64StringSerializer::class)
        val data: ByteString,
    ) : HarWebSocketMessage()
}

@Serializable
enum class WebSocketDirection {
    /** The message was sent from this client. */
    @SerialName("send")
    Send,

    /** The message was received from the server. */
    @SerialName("receive")
    Receive,
}

/**
 * Handles web socket message types polymorphism based on the `opcode` JSON property (1 for text, 2 for binary).
 */
private object WebSocketMessageSerializer : JsonContentPolymorphicSerializer(HarWebSocketMessage::class) {

    override fun selectDeserializer(element: JsonElement): DeserializationStrategy {
        val opcodeElement = element.jsonObject["opcode"] ?: error("Missing 'opcode' property in web socket message")
        return when(val opcode = opcodeElement.jsonPrimitive.int) {
            1 -> HarWebSocketMessage.Text.serializer()
            2 -> HarWebSocketMessage.Binary.serializer()
            else -> error("Unknown web socket opcode $opcode")
        }
    }
}

/**
 * Serializes [ByteString]s as a fractional number of epoch seconds (a double in JSON).
 */
@OptIn(ExperimentalEncodingApi::class)
private object ByteStringAsBase64StringSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ByteString", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: ByteString) {
        val base64String = Base64.encode(value)
        encoder.encodeString(base64String)
    }

    override fun deserialize(decoder: Decoder): ByteString {
        val base64String = decoder.decodeString()
        return Base64.decodeToByteString(base64String)
    }
}

private const val NANOS_IN_SECOND = 1_000_000_000

/**
 * Serializes [Instant]s as a fractional number of epoch seconds (a double in JSON).
 */
private object InstantAsDoubleSecondsSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.DOUBLE)

    override fun serialize(encoder: Encoder, value: Instant) {
        val doubleSeconds = value.epochSeconds.toDouble() + value.nanosecondsOfSecond.toDouble() / NANOS_IN_SECOND
        encoder.encodeDouble(doubleSeconds)
    }

    override fun deserialize(decoder: Decoder): Instant {
        val doubleSeconds = decoder.decodeDouble()
        val fullSeconds = doubleSeconds.toLong()
        val nanos = (doubleSeconds - fullSeconds) * NANOS_IN_SECOND
        return Instant.fromEpochSeconds(fullSeconds, nanos.toInt())
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy