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

Class Method, % Branch, % Line, % Instruction, %
GhostConverterFactory 100% (9/9) 78.9% (30/38) 94.2% (65/69) 96.3% (284/295)
GhostConverterFactory$Companion 100% (1/1) 100% (1/1) 100% (4/4)
Total 100% (10/10) 78.9% (30/38) 94.3% (66/70) 96.3% (288/299)


 @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.ghostInternalUseFlatReader
 import com.ghost.serialization.releaseScratchBuffer
 import com.ghost.serialization.serializers.ListSerializer
 import com.ghost.serialization.serializers.MapSerializer
 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.ParameterizedType
 import java.lang.reflect.Type
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.reflect.KClass
 
 /**
  * Retrofit Converter Factory for Ghost Serialization.
  */
 @OptIn(InternalGhostApi::class)
 class GhostConverterFactory 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
 
         val isStrict = annotations.any { it is com.ghost.serialization.annotations.GhostStrict }
         val isCoerce = annotations.any { it is com.ghost.serialization.annotations.GhostCoerce }
 
         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
                     }
 
                     ghostInternalUseFlatReader(
                         scratch, offset
                     ) { reader ->
                         reader.strictMode = isStrict
                         if (isCoerce) {
                             reader.coerceStringsToNumbers = true
                             reader.coerceBooleans = true
                         }
                         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
         }
 
         val serializer = getSerializerForType(type) ?: return null
         val existing = serializerCache.putIfAbsent(type, serializer)
         return existing ?: serializer
     }
 
     private fun getSerializerForType(type: Type): GhostSerializer<Any>? {
         if (type is Class<*>) {
             return Ghost
                 .getSerializer(type.kotlin as KClass<Any>)
         }
 
         if (type is ParameterizedType) {
             val rawType = type.rawType as? Class<*> ?: return null
 
             if (List::class.java.isAssignableFrom(rawType)) {
                 val arg = type.actualTypeArguments.firstOrNull()
                     ?: return null
 
                 val itemSerializer = getSerializerWithCache(arg)
                     ?: return null
 
                 return ListSerializer(itemSerializer) as GhostSerializer<Any>
             }
 
             if (Map::class.java.isAssignableFrom(rawType)) {
                 val arg = type.actualTypeArguments.getOrNull(1)
                     ?: return null
 
                 val valueSerializer = getSerializerWithCache(arg)
                     ?: return null
 
                 return MapSerializer(valueSerializer) as GhostSerializer<Any>
             }
 
         }
         return null
     }
 
     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(): GhostConverterFactory = GhostConverterFactory()
     }
 }