Coverage Summary for Class: GhostProtoConverterFactory (com.ghost.serialization.retrofit)

Class Method, % Branch, % Line, % Instruction, %
GhostProtoConverterFactory 100% (7/7) 88.9% (16/18) 94.9% (37/39) 97.9% (185/189)
GhostProtoConverterFactory$Companion 100% (1/1) 100% (1/1) 100% (4/4)
Total 100% (8/8) 88.9% (16/18) 95% (38/40) 97.9% (189/193)


 @file:Suppress("UNCHECKED_CAST")
 
 package com.ghost.serialization.retrofit
 
 import com.ghost.serialization.Ghost
 import com.ghost.serialization.InternalGhostApi
 import com.ghost.serialization.acquireScratchBuffer
 import com.ghost.serialization.contract.GhostSerializer
 import com.ghost.serialization.parser.GhostProtoJsonFlatReader
 import com.ghost.serialization.parser.consumeNull
 import com.ghost.serialization.parser.isNextNullValue
 import com.ghost.serialization.releaseScratchBuffer
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.RequestBody
 import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.ResponseBody
 import retrofit2.Converter
 import retrofit2.Retrofit
 import java.lang.reflect.Type
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.reflect.KClass
 
 /**
  * Retrofit `Converter.Factory` for proto3 JSON mapping (`@GhostProtoSerialization`).
  *
  * Differs from [GhostConverterFactory] only on the read path: 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 — required for round-tripping payloads produced by real protobuf/JSON
  * libraries. Encoding (`requestBodyConverter`) reuses [Ghost.encodeToBytes] since proto3 wire
  * correctness (int64 quoting, Base64 `bytes`, default-value omission) is generated directly
  * into the `@GhostProtoSerialization` serializer's own `serialize()` method.
  *
  * Scope: direct (non-generic) types only — unlike [GhostConverterFactory], this does not
  * unwrap `List<T>`/`Map<K, V>` response/request bodies.
  *
  * ```kotlin
  * Retrofit.Builder()
  *     .baseUrl(baseUrl)
  *     .addConverterFactory(GhostProtoConverterFactory.create())
  *     .build()
  * ```
  */
 @OptIn(InternalGhostApi::class)
 class GhostProtoConverterFactory private constructor() : Converter.Factory() {
 
     private val serializerCache = ConcurrentHashMap<Type, GhostSerializer<Any>>()
 
     override fun responseBodyConverter(
         type: Type,
         annotations: Array<out Annotation>,
         retrofit: Retrofit
     ): Converter<ResponseBody, *>? {
         val serializer = getSerializerWithCache(type)
             ?: return null
 
         return Converter { body ->
             body.use {
                 val stream = it.byteStream()
                 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 = stream.read(scratch, offset, scratch.size - offset)
                         if (read == -1) break
                         offset += read
                     }
 
                     val reader = GhostProtoJsonFlatReader(scratch)
                     reader.limit = offset
                     if (reader.isNextNullValue()) {
                         reader.consumeNull()
                         null
                     } else {
                         serializer.deserialize(reader)
                     }
                 } finally {
                     releaseScratchBuffer(scratch)
                 }
             }
         }
     }
 
     override fun requestBodyConverter(
         type: Type,
         parameterAnnotations: Array<out Annotation>,
         methodAnnotations: Array<out Annotation>,
         retrofit: Retrofit
     ): Converter<*, RequestBody>? {
         val serializer = getSerializerWithCache(type)
             ?: return null
 
         return Converter<Any, RequestBody> { value ->
             Ghost.encodeToBytes(serializer, value)
                 .toRequestBody(MEDIA_TYPE)
         }
     }
 
     private fun getSerializerWithCache(type: Type): GhostSerializer<Any>? {
         val cached = serializerCache[type]
         if (cached != null) {
             return cached
         }
         if (type !is Class<*>) {
             return null
         }
         val serializer = Ghost.getSerializer(type.kotlin as KClass<Any>) ?: return null
         val existing = serializerCache.putIfAbsent(type, serializer)
         return existing ?: serializer
     }
 
     companion object {
         private const val STR_MEDIA_TYPE = "application/json; charset=UTF-8"
         private val MEDIA_TYPE = STR_MEDIA_TYPE.toMediaType()
         private const val BUFFER_SIZE = 524288
 
         fun create(): GhostProtoConverterFactory = GhostProtoConverterFactory()
     }
 }