Coverage Summary for Class: FrameCodec (it.unibo.tuprolog.argumentation.parallel.federation)

Class Class, % Method, % Branch, % Line, % Instruction, %
FrameCodec 100% (1/1) 100% (3/3) 100% (26/26) 97.7% (42/43) 98.4% (361/367)


 package it.unibo.tuprolog.argumentation.parallel.federation
 
 import it.unibo.tuprolog.argumentation.parallel.message.TextReader
 import it.unibo.tuprolog.argumentation.parallel.message.TextWriter
 
 /** What sites send one another on their links, as text (see [FrameCodec]) */
 internal sealed interface Frame {
     /**
      * The greeting of a site, on each link: the site, its version (sites of different versions do not federate), the
      * workers it runs (null for no limit), whether it lends them to its peers, and whether it opens links of its own to
      * the sites it is introduced to (as browsers do, over WebRTC). The first one on a link carries as well the [nonce]
      * the other site proves its secret on (a code, or the token of a pairing), the [proof] of the secret of this one on
      * the nonce of the other (see [Pairing.proof]), and, from a site paired just now by its code, the [token] of the
      * pairing. Neither a code nor a token is ever sent but in that last case.
      */
     data class Hello(
         val site: String,
         val version: String,
         val capacity: Int?,
         val lends: Boolean,
         val introducible: Boolean = false,
         val nonce: String? = null,
         val proof: String? = null,
         val token: String? = null,
     ) : Frame
 
     /**
      * The first frame on a link from the site that did not open it (see [Link.outgoing]): the [nonce] the other proves
      * its code or token on, greeting it then
      */
     data class Challenge(
         val nonce: String,
     ) : Frame
 
     /** To the site that opened a link: the link is refused (the code it presented is not that of this site), why */
     data class Refused(
         val reason: String,
     ) : Frame
 
     /** To a site opening links of its own: [site] does as well, and is reached through the sender of the frame */
     data class Introduce(
         val site: String,
     ) : Frame
 
     /** From [from] to [to], through the sites in between: what the two need to open a link of their own */
     data class Signal(
         val from: String,
         val to: String,
         val payload: String,
     ) : Frame
 
     /** The sites of the workers of a graph [session] builds, by their addresses: those of older sessions are dropped */
     data class Placement(
         val session: String,
         val workers: Map<String, String>,
     ) : Frame
 
     /** To a delegate: [site] left, the workers of its sessions are of no use any more */
     data class Forget(
         val site: String,
     ) : Frame
 
     /** To the site of [session]: a link its messages went through closed, some may be lost */
     data class Lost(
         val session: String,
     ) : Frame
 
     /** A message of [session] to the node [to], written by MessageCodec */
     data class Deliver(
         val session: String,
         val to: String,
         val message: String,
     ) : Frame
 }
 
 /** The frames as text: their fields as MessageCodec writes those of the messages */
 internal object FrameCodec {
     fun encode(frame: Frame): String =
         TextWriter()
             .apply {
                 when (frame) {
                     is Frame.Hello ->
                         text("Hello")
                             .text(frame.site)
                             .text(frame.version)
                             .number(frame.capacity ?: -1)
                             .text(if (frame.lends) "1" else "0")
                             .text(if (frame.introducible) "1" else "0")
                             .texts(listOfNotNull(frame.nonce))
                             .texts(listOfNotNull(frame.proof))
                             .texts(listOfNotNull(frame.token))
                     is Frame.Challenge -> text("Challenge").text(frame.nonce)
                     is Frame.Refused -> text("Refused").text(frame.reason)
                     is Frame.Introduce -> text("Introduce").text(frame.site)
                     is Frame.Signal -> text("Signal").text(frame.from).text(frame.to).text(frame.payload)
                     is Frame.Placement ->
                         text("Placement").text(frame.session).list(frame.workers.entries.toList()) { text(it.key).text(it.value) }
                     is Frame.Forget -> text("Forget").text(frame.site)
                     is Frame.Lost -> text("Lost").text(frame.session)
                     is Frame.Deliver -> text("Deliver").text(frame.session).text(frame.to).text(frame.message)
                 }
             }.toString()
 
     /** The frame of [text]: an IllegalArgumentException if it is none */
     fun decode(text: String): Frame =
         TextReader(text).run {
             when (val kind = text()) {
                 "Hello" ->
                     Frame.Hello(
                         text(),
                         text(),
                         number().takeIf { it >= 0 },
                         text() == "1",
                         text() == "1",
                         optional(),
                         optional(),
                         optional(),
                     )
                 "Challenge" -> Frame.Challenge(text())
                 "Refused" -> Frame.Refused(text())
                 "Introduce" -> Frame.Introduce(text())
                 "Signal" -> Frame.Signal(text(), text(), text())
                 "Placement" -> Frame.Placement(text(), list { text() to text() }.toMap())
                 "Forget" -> Frame.Forget(text())
                 "Lost" -> Frame.Lost(text())
                 "Deliver" -> Frame.Deliver(text(), text(), text())
                 else -> throw IllegalArgumentException("Unknown frame $kind")
             }
         }
 
     // A field written as a list of none or one text
     private fun TextReader.optional(): String? = texts().singleOrNull()
 }
 
 /**
  * The longest frame a site receives, in bytes: a longer one closes its link, rather than filling the memory of the
  * site (see the transports)
  */
 internal const val LONGEST_FRAME: Int = 64 * 1024 * 1024
 
 /**
  * The chunks of [frame], of at most [size] characters, one at least, for the transports bounding their messages: none
  * ends between the two halves of a character beyond 16 bits (a surrogate pair), which a transport sending UTF-8 would
  * not carry
  */
 internal fun chunked(
     frame: String,
     size: Int,
 ): List<String> {
     val chunks = mutableListOf<String>()
     var start = 0
     while (start < frame.length) {
         var end = minOf(start + size, frame.length)
         if (end < frame.length && end - 1 > start && frame[end - 1].isHighSurrogate()) end--
         chunks += frame.substring(start, end)
         start = end
     }
     return chunks.ifEmpty { listOf("") }
 }