Coverage Summary for Class: GhostHttpMessageConverter (com.ghost.serialization.spring)

Class Class, % Method, % Branch, % Line, % Instruction, %
GhostHttpMessageConverter 100% (1/1) 100% (6/6) 69.2% (18/26) 86.3% (44/51) 83.1% (212/255)


 package com.ghost.serialization.spring
 
 import com.ghost.serialization.Ghost
 import com.ghost.serialization.contract.GhostSerializer
 import com.ghost.serialization.parser.GhostProtoJsonFlatReader
 import org.springframework.http.HttpInputMessage
 import org.springframework.http.HttpOutputMessage
 import org.springframework.http.MediaType
 import org.springframework.http.converter.AbstractHttpMessageConverter
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.reflect.KClass
 
 /**
  * Spring [org.springframework.http.converter.HttpMessageConverter]
  * implementation that uses Ghost Serialization.
  *
  * **Read path:** Extracts the request body as a [ByteArray] and feeds it
  * directly to the pooled [com.ghost.serialization.parser.GhostJsonReader],
  * avoiding intermediate Okio wrappers.
  *
  * **Write path:** Serializes through the pooled monomorphic
  * [com.ghost.serialization.writer.GhostJsonFlatWriter] and writes the
  * resulting [ByteArray] in a single bulk call to the output stream,
  * bypassing Okio sink wrapping entirely.
  */
 class GhostHttpMessageConverter : AbstractHttpMessageConverter<Any>(
     MediaType.APPLICATION_JSON,
     MediaType("application", "*+json")
 ) {
     private val supportsCache = ConcurrentHashMap<Class<*>, Boolean>()
     private val serializerCache = ConcurrentHashMap<Class<*>, GhostSerializer<Any>>()
 
     override fun supports(clazz: Class<*>): Boolean {
         val cached = supportsCache[clazz]
         if (cached != null) return cached
 
         val result = if (isExcludedType(clazz)) {
             false
         } else {
             Ghost.getSerializer(clazz.kotlin) != null
         }
         supportsCache[clazz] = result
         return result
     }
 
     /**
      * Prevents Ghost from intercepting standard primitive and java.lang types.
      * This ensures Spring falls back to its default converters (e.g. StringHttpMessageConverter)
      * for endpoints returning text/plain, raw bytes, or basic values, preserving their correct HTTP formats.
      *
      * Extracted to a clean helper to facilitate JVM JIT compilation and inlining.
      */
     private fun isExcludedType(clazz: Class<*>): Boolean {
         return clazz == String::class.java ||
                 clazz == ByteArray::class.java ||
                 clazz.isPrimitive ||
                 clazz.name.startsWith("java.lang.")
     }
 
     override fun readInternal(clazz: Class<out Any>, inputMessage: HttpInputMessage): Any {
         val bytes = inputMessage.body.readBytes()
         val isStrict = GhostSpringConfig.strict.get()
         val isCoerce = GhostSpringConfig.coerce.get()
 
         @Suppress("UNCHECKED_CAST")
         val targetClass = clazz as Class<Any>
         var serializer = serializerCache[targetClass]
         if (serializer == null) {
             serializer = Ghost.getSerializer(targetClass.kotlin as KClass<Any>) as GhostSerializer<Any>?
                 ?: Ghost.throwError("${Ghost.NOT_FOUND} ${targetClass.simpleName}")
             serializerCache[targetClass] = serializer
         }
 
         // @GhostProtoSerialization classes need GhostProtoJsonFlatReader's proto3 leniency
         // (quoted-or-bare int64/uint64, lenient int32, quoted NaN/Infinity). The annotation
         // itself is BINARY-retained (invisible to reflection), so `serializer.isProto` is how
         // this shared, globally-registered converter tells the two cases apart.
         if (serializer.isProto) {
             val reader = GhostProtoJsonFlatReader(bytes)
             reader.strictMode = isStrict
             if (isCoerce) {
                 reader.coerceStringsToNumbers = true
                 reader.coerceBooleans = true
             }
             return serializer.deserialize(reader)
         }
 
         return com.ghost.serialization.ghostInternalUseFlatReader(bytes) { reader ->
             reader.strictMode = isStrict
             if (isCoerce) {
                 reader.coerceStringsToNumbers = true
                 reader.coerceBooleans = true
             }
             serializer.deserialize(reader)
         }
     }
 
     override fun writeInternal(t: Any, outputMessage: HttpOutputMessage) {
         val clazz = t.javaClass
         var serializer = serializerCache[clazz]
         if (serializer == null) {
             @Suppress("UNCHECKED_CAST")
             serializer = Ghost.getSerializer(clazz.kotlin as KClass<Any>) as GhostSerializer<Any>?
                 ?: throw IllegalArgumentException(
                     "${Ghost.NOT_FOUND} ${clazz.simpleName}. ${Ghost.MISSING_ANN}"
                 )
             serializerCache[clazz] = serializer
         }
 
         @Suppress("UNCHECKED_CAST")
         GhostStarterHelper.writeToStream(
             t,
             serializer,
             outputMessage.body
         )
         outputMessage.body.flush()
     }
 }