Coverage Summary for Class: GhostProtoContentConverter (com.ghost.serialization.ktor)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostProtoContentConverter |
75%
(3/4)
|
70%
(14/20)
|
89.3%
(25/28)
|
85.7%
(156/182)
|
| GhostProtoContentConverter$Companion |
|
| GhostProtoContentConverter$deserialize$1 |
|
| Total |
75%
(3/4)
|
70%
(14/20)
|
89.3%
(25/28)
|
85.7%
(156/182)
|
package com.ghost.serialization.ktor
import com.ghost.serialization.Ghost
import com.ghost.serialization.InternalGhostApi
import com.ghost.serialization.acquireScratchBuffer
import com.ghost.serialization.releaseScratchBuffer
import com.ghost.serialization.parser.GhostProtoJsonFlatReader
import io.ktor.http.ContentType
import io.ktor.http.content.ByteArrayContent
import io.ktor.http.content.OutgoingContent
import io.ktor.serialization.ContentConverter
import io.ktor.util.reflect.TypeInfo
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.charsets.Charset
import kotlin.reflect.KClass
/**
* Ktor [ContentConverter] for proto3 JSON mapping (`@GhostProtoSerialization`).
*
* Differs from [GhostContentConverter] only on the read path: request/response bodies are
* parsed through [GhostProtoJsonFlatReader], which additionally accepts quoted-or-bare
* int64/uint64, lenient int32 (rejects fractional values), and quoted `"NaN"`/`"Infinity"`
* literals per proto3 JSON rules. `serializeNullable` reuses [Ghost.encodeToBytes] since wire
* correctness on write is already generated into the `@GhostProtoSerialization` serializer's
* own `serialize()` method.
*
* ```kotlin
* install(ContentNegotiation) { ghostProto() }
* ```
*/
@OptIn(InternalGhostApi::class)
class GhostProtoContentConverter(
private val configurer: ((GhostProtoJsonFlatReader) -> Unit)? = null
) : ContentConverter {
@Suppress("UNCHECKED_CAST")
override suspend fun serializeNullable(
contentType: ContentType,
charset: Charset,
typeInfo: TypeInfo,
value: Any?
): OutgoingContent? {
if (value == null) return null
val clazz = typeInfo.type
val serializer = typeInfo.kotlinType?.let { Ghost.getSerializer(it) }
?: Ghost.getSerializer(clazz as KClass<Any>)
?: return null
val bytes = Ghost.encodeToBytes(serializer, value)
return ByteArrayContent(bytes, contentType)
}
@Suppress("UNCHECKED_CAST")
override suspend fun deserialize(
charset: Charset,
typeInfo: TypeInfo,
content: ByteReadChannel
): Any? {
val serializer = typeInfo.kotlinType?.let { Ghost.getSerializer(it) }
?: Ghost.getSerializer(typeInfo.type as KClass<Any>)
?: return null
var scratch = acquireScratchBuffer(BUFFER_SIZE)
try {
var offset = 0
while (true) {
if (offset == scratch.size) {
val grown = acquireScratchBuffer(scratch.size * 2)
scratch.copyInto(grown, 0, 0, offset)
releaseScratchBuffer(scratch)
scratch = grown
}
val read = content.readAvailable(scratch, offset, scratch.size - offset)
if (read == -1) break
offset += read
}
val reader = GhostProtoJsonFlatReader(scratch)
reader.limit = offset
configurer?.invoke(reader)
return serializer.deserialize(reader)
} finally {
releaseScratchBuffer(scratch)
}
}
companion object {
private const val BUFFER_SIZE = 524288
}
}