Coverage Summary for Class: BaseDeserializeEmitter (com.ghost.serialization.compiler)

Class Class, % Method, % Branch, % Line, % Instruction, %
BaseDeserializeEmitter 100% (1/1) 78.9% (15/19) 66% (103/156) 69.7% (154/221) 68% (922/1356)


 package com.ghost.serialization.compiler
 
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.ksp.toTypeName
 import com.ghost.serialization.compiler.GhostEmitterConstants as C
 
 /**
  * Abstract base class for all deserialization emitters within the Ghost compiler.
  *
  * This class serves as the foundation for generating deserialization logic. It manages
  * shared state and provides a suite of helper methods to transform KSP symbols into
  * executable [CodeBlock] instructions via KotlinPoet.
  *
  * ### Responsibilities:
  * - **Schema Validation:** Pre-calculates [requiredMasks] and [defaultMasks] using bitwise operations
  * to ensure data integrity during runtime with minimal CPU overhead.
  * - **Path Resolution:** Flattens nested property paths (e.g., "user.profile.id") to allow mapping
  * deeply nested JSON structures directly to flat DTOs.
  * - **Type Resolution:** Encapsulates the logic for determining which deserialization strategy
  * to apply (primitives vs. lists vs. recursive calls to other serializers).
  * - **Code Generation Helpers:** Provides common templates for `null` handling, custom decoders,
  * and contextual serializer injection.
  *
  * @param properties The list of [GhostPropertyModel] describing the fields of the target class.
  * @param originalClassName The [ClassName] of the DTO being deserialized.
  * @param readerClass The [ClassName] of the reader being used (e.g., GhostJsonReader or GhostJsonFlatReader).
  */
 internal abstract class BaseDeserializeEmitter(
     protected val properties: List<GhostPropertyModel>,
     protected val originalClassName: ClassName,
     protected val readerClass: ClassName
 ) {
 
     protected val contextualSerializers = mutableMapOf<KSType, String>()
 
     protected val maskCount = (properties.size + C.MASK_SIZE_BITS_MINUS_ONE) /
             C.MASK_SIZE_BITS.toInt()
 
     /**
      * Resolves the flatten path for nested properties.
      * This allows Ghost to map deeply nested JSON structures (e.g., "user.profile.id")
      * directly to a flat DTO field without needing intermediate data classes.
      * This is an optimization to keep the domain model clean while maintaining
      * compatibility with non-flat JSON structures.
      */
     protected val fullPaths = properties.map {
         it.flattenPath ?: (it.wrapPath?.let { path ->
             path + it.jsonName
         } ?: listOf(it.jsonName))
     }
 
     /**
      * Maps each property model to its unique zero-based index.
      * This is essential for calculating the maskIdx and bitIdx used in bitwise operations.
      */
     protected val propertyIndices = properties
         .mapIndexed { index, prop -> prop to index }
         .toMap()
 
     /**
      * Calculates the validation bitmask for required fields of the DTO.
      *
      * This property identifies which fields must be present in the JSON during deserialization.
      * A field is considered mandatory if:
      * 1. It is not nullable (`!isNullable`).
      * 2. It does not have a default value (`!hasDefaultValue`).
      *
      * **Validation Architecture:**
      * The bits are distributed across a [LongArray] to support schemas with more than 64 properties.
      * Each field is assigned to a position (maskIdx, bitIdx) based on its index in the property list.
      *
      * During runtime, the generated serializer uses this mask to perform high-performance schema validation.
      * By comparing the received mask against this required mask using a binary AND operation
      * (`mask & requiredMask == requiredMask`), we validate the object integrity in a single CPU cycle.
      *
      * @return A [LongArray] where each set bit represents a mandatory field that must be validated
      * after the deserialization process completes.
      */
     protected val requiredMasks: LongArray by lazy {
         val masks = LongArray(maskCount)
         properties.forEachIndexed { index, prop ->
             if (!prop.isNullable && !prop.hasDefaultValue) {
                 val maskIdx = index / C.MASK_SIZE_BITS.toInt()
                 val bitIdx = index % C.MASK_SIZE_BITS.toInt()
                 masks[maskIdx] = masks[maskIdx] or (1L shl bitIdx)
             }
         }
         masks
     }
 
     /**
      * Calculates the bitmask for properties that have defined default values.
      *
      * This mask identifies fields for which the generated deserializer can safely
      * fall back to the class-level default values if the property is missing from the JSON input.
      *
      * **Architectural Purpose:**
      * During the final return/instantiation stage, the generated deserializer uses this mask
      * to perform "Constructor Dispatching". If a field is missing, instead of throwing an error
      * (as it would for [requiredMasks]), it relies on this mask to know that it is safe
      * to omit the argument from the constructor call, allowing Kotlin's default
      * parameter logic to take over.
      *
      * @return A [LongArray] where each set bit represents a property that possesses a
      * default value, assisting in optimal constructor selection and object instantiation.
      */
     protected val defaultMasks: LongArray by lazy {
         val masks = LongArray(maskCount)
         properties.forEachIndexed { index, prop ->
             if (prop.hasDefaultValue) {
                 val maskIdx = index / C.MASK_SIZE_BITS.toInt()
                 val bitIdx = index % C.MASK_SIZE_BITS.toInt()
                 masks[maskIdx] = masks[maskIdx] or (1L shl bitIdx)
             }
         }
         masks
     }
 
     /**
      * Formats a bitmask into a constant string literal for code generation.
      * Handles edge cases like [Long.MIN_VALUE] which requires specific formatting.
      */
     protected fun formatMaskString(mask: Long): String {
         return if (mask == Long.MIN_VALUE) {
             C.STR_BIT_MASK_MIN_LONG
         } else {
             C.FMT_LONG_LITERAL.format(mask)
         }
     }
 
     /**
      * The main entry point for generating the reader call for a specific property.
      * Handles dispatching to specialized logic based on property attributes
      * (e.g., custom decoders, nullability, sealed classes).
      */
     protected fun buildCall(prop: GhostPropertyModel): CodeBlock {
         if (prop.customDecoder != null) {
             return buildCustomDecoderCall(prop)
         }
         if (prop.isNullable) {
             return buildNullableCall(prop)
         }
 
         return when {
             prop.isValueClass && prop.valueClassProperty != null -> {
                 buildCall(prop.valueClassProperty)
             }
 
             prop.isSealedClass -> CodeBlock.of(
                 C.TEMPLATE_DESERIALIZE_T,
                 prop.type.serializerClassName()
             )
 
             prop.isPrimitiveArray -> CodeBlock.of(
                 C.TEMPLATE_DESERIALIZE_T,
                 ClassName(
                     C.STR_SERIALIZERS_PKG,
                     "${prop.primitiveArrayType}${C.STR_SERIALIZER_SUFFIX}"
                 )
             )
 
             prop.isContextual -> {
                 val name = getContextualSerializerName(prop.type)
                 CodeBlock.of(C.TEMPLATE_DESERIALIZE_L, name)
             }
 
             prop.isProto && prop.type.isPrimitiveLong() -> CodeBlock.of(C.STR_NEXT_LONG_PROTO_COERCED)
 
             prop.isProto && prop.type.isByteArray() -> CodeBlock.of(C.STR_DECODE_BASE64_STRING_CALL)
 
             else -> buildTypeReaderCall(prop.type, prop.isProto)
         }
     }
 
     /**
      * Generates the code block for a nullable property.
      * It handles null-safety by wrapping the reader call in a null check template.
      */
     protected fun buildNullableCall(prop: GhostPropertyModel): CodeBlock {
         if (prop.customDecoder != null) {
             return nullGuarded(buildCustomDecoderCall(prop))
         }
 
         if (prop.isPrimitiveArray) {
             return nullGuarded(
                 CodeBlock.of(
                     C.TEMPLATE_DESERIALIZE_T,
                     ClassName(
                         C.STR_SERIALIZERS_PKG,
                         "${prop.primitiveArrayType}${C.STR_SERIALIZER_SUFFIX}"
                     )
                 )
             )
         }
 
         if (prop.isProto && prop.type.isPrimitiveLong()) {
             return CodeBlock.of(C.STR_NEXT_LONG_PROTO_COERCED)
         }
 
         if (prop.isProto && prop.type.isByteArray()) {
             return CodeBlock.of(C.STR_DECODE_BASE64_STRING_CALL)
         }
 
         return buildTypeReaderCall(prop.type, prop.isProto)
     }
 
     /**
      * Generates code for custom decoder implementations.
      * Handles the transition between standard reader and the custom decoding logic.
      */
     protected fun buildCustomDecoderCall(prop: GhostPropertyModel): CodeBlock {
         val coder = prop.customDecoder!!
         if (usesDirectCustomDecoderCall(coder)) {
             return CodeBlock.of(C.TEMPLATE_L_READER, coder.provider, coder.functionName)
         }
         return when (readerClass.simpleName) {
             C.STR_GHOST_JSON_FLAT_READER -> buildFlatReaderCustomDecoderBridge(coder)
             C.STR_GHOST_JSON_STRING_READER -> buildStringReaderCustomDecoderBridge(coder)
             else -> buildFlatReaderCustomDecoderBridge(coder)
         }
     }
 
     private fun usesDirectCustomDecoderCall(coder: CustomCoderModel): Boolean {
         val channelKind = when (readerClass.simpleName) {
             C.STR_GHOST_JSON_STRING_READER -> CustomCoderReaderKind.STRING
             C.STR_GHOST_JSON_FLAT_READER -> CustomCoderReaderKind.FLAT
             else -> CustomCoderReaderKind.BYTES
         }
         return coder.supports(channelKind)
     }
 
     private fun buildFlatReaderCustomDecoderBridge(coder: CustomCoderModel): CodeBlock {
         return CodeBlock.builder()
             .add(C.STR_RUN_OPEN)
             .add(C.STR_CUSTOM_DECODER_TEMP_READER)
             .add(C.TEMPLATE_CUSTOM_DECODER_TEMP_CALL, coder.provider, coder.functionName)
             .add(C.STR_CUSTOM_DECODER_UPDATE_POS)
             .add(C.STR_RESET_TOKEN_BYTE_CALL)
             .add(C.STR_CUSTOM_DECODER_RETURN_RES)
             .add(C.STR_RUN_CLOSE)
             .build()
     }
 
     private fun buildStringReaderCustomDecoderBridge(coder: CustomCoderModel): CodeBlock {
         return CodeBlock.builder()
             .add(C.STR_RUN_OPEN)
             .add(C.STR_CUSTOM_DECODER_TEMP_READER_STRING)
             .add(C.TEMPLATE_CUSTOM_DECODER_TEMP_CALL, coder.provider, coder.functionName)
             .add(C.STR_CUSTOM_DECODER_UPDATE_POS_STRING)
             .add(C.STR_CUSTOM_DECODER_RETURN_RES)
             .add(C.STR_RUN_CLOSE)
             .build()
     }
 
     /**
      * Dispatches the correct [CodeBlock] to read a specific [KSType] from the JSON reader.
      * * This acts as the recursive entry point for the emitter. It follows these resolution rules:
      * 1. **Ghost/Enum types:** Delegates to an existing serializer (recursive call).
      * 2. **Primitives:** Maps directly to optimized `GhostJsonReader` methods (e.g., [kotlin.random.Random.Default.nextInt]).
      * 3. **Collections (List/Map):** Applies a recursive template to handle generic types.
      * 4. **Fallbacks:** Uses contextual resolution for unknown or custom types.
      *
      * @param type The KSP type being resolved.
      * @param isProto True when the enclosing class is `@GhostProtoSerialization` — propagated
      *   into `List`/`Set`/`Map` element recursion so `Long`/`ByteArray` elements also get
      *   proto3 quoted-int64/Base64 decoding.
      * @return A [CodeBlock] containing the optimized reader instruction.
      */
     protected fun buildTypeReaderCall(type: KSType, isProto: Boolean = false): CodeBlock {
         val readerCall = when {
             type.isRawJson() -> CodeBlock.of(C.STR_RAW_JSON_FROM_CAPTURE)
 
             type.isByteArray() -> if (isProto) {
                 CodeBlock.of(C.STR_DECODE_BASE64_STRING_CALL)
             } else {
                 CodeBlock.of(C.STR_CAPTURE_RAW_JSON_BYTES)
             }
 
             isValueClassType(type) -> {
                 val innerType = resolveValueClassInnerType(type)
                 if (innerType != null) {
                     val constructorCall = buildTypeReaderCall(innerType, isProto)
                     val className = type.declaration.qualifiedName?.asString()?.let { ClassName.bestGuess(it) } ?: type.toTypeName()
                     CodeBlock.of(C.TEMPLATE_CONSTRUCTOR, className, constructorCall)
                 } else {
                     val name = getContextualSerializerName(type)
                     CodeBlock.of(C.TEMPLATE_DESERIALIZE_L, name)
                 }
             }
 
             type.isGhost() || type.isEnum() -> CodeBlock.of(
                 C.TEMPLATE_DESERIALIZE_T,
                 type.serializerClassName()
             )
 
             type.isPrimitiveInt() -> CodeBlock.of(C.STR_NEXT_INT)
             type.isPrimitiveBoolean() -> CodeBlock.of(C.STR_NEXT_BOOLEAN)
             type.isPrimitiveLong() -> if (isProto) {
                 CodeBlock.of(C.STR_NEXT_LONG_PROTO_COERCED)
             } else {
                 CodeBlock.of(C.STR_NEXT_LONG)
             }
             type.isPrimitiveDouble() -> CodeBlock.of(C.STR_NEXT_DOUBLE)
             type.isPrimitiveFloat() -> CodeBlock.of(C.STR_NEXT_FLOAT)
             type.isPrimitiveByte() -> CodeBlock.of(C.STR_NEXT_BYTE)
             type.isPrimitiveShort() -> CodeBlock.of(C.STR_NEXT_SHORT)
             type.isPrimitiveChar() -> CodeBlock.of(C.STR_NEXT_CHAR)
 
             type.isSet() -> {
                 val inner = type.arguments.firstOrNull()?.type?.resolve()
                     ?: return CodeBlock.of(C.STR_NEXT_STRING)
 
                 CodeBlock.of(
                     C.STR_READ_SET_TEMPLATE,
                     buildTypeReaderCall(inner, isProto)
                 )
             }
 
             type.isList() -> {
                 val inner = type.arguments.firstOrNull()?.type?.resolve()
                     ?: return CodeBlock.of(C.STR_NEXT_STRING)
 
                 CodeBlock.of(
                     C.STR_READ_LIST_TEMPLATE,
                     buildTypeReaderCall(inner, isProto)
                 )
 
             }
 
             type.isMap() -> {
                 val valueType = type
                     .arguments
                     .getOrNull(1)
                     ?.type?.resolve()
                     ?: return CodeBlock.of(C.STR_NEXT_STRING)
 
                 CodeBlock.of(
                     C.STR_READ_MAP_TEMPLATE,
                     buildTypeReaderCall(valueType, isProto)
                 )
             }
 
             else -> {
                 if (type.isString()) {
                     CodeBlock.of(C.STR_NEXT_STRING)
                 } else {
                     val name = getContextualSerializerName(type)
                     CodeBlock.of(C.TEMPLATE_DESERIALIZE_L, name)
                 }
             }
         }
 
         return if (type.isMarkedNullable) {
             nullGuarded(readerCall)
         } else {
             readerCall
         }
     }
 
     /**
      * Generates a unique variable name for a contextual serializer.
      * Example: "User" -> "contextualUserSerializer".
      */
     private fun getContextualSerializerName(type: KSType): String {
         return contextualSerializers.getOrPut(type) {
             val simpleName = type.declaration.simpleName.asString()
             val nullableSuffix = if (type.isMarkedNullable) "Nullable" else ""
             C.STR_CONTEXTUAL_PREFIX +
                     simpleName.replaceFirstChar { it.lowercase() } +
                     nullableSuffix +
                     C.STR_SERIALIZER_SUFFIX
         }
     }
 
     /**
      * Registers descriptive private bitmask constants on the companion/object builder
      * for every property and all validation/defaults masks, completely eliminating magic numbers.
      *
      * @param typeSpecBuilder The [TypeSpec.Builder] where the serializer properties will be added.
      */
     protected fun emitPropertyMaskConstants(typeSpecBuilder: TypeSpec.Builder) {
         properties.forEach { prop ->
             val index = propertyIndices[prop]!!
             val bitIdx = index % C.MASK_SIZE_BITS.toInt()
             val bitMask = C.VAL_ONE_L shl bitIdx
             val bitMaskStr = formatMaskString(bitMask)
             val name = "MASK_" + prop.kotlinName.uppercase()
             if (typeSpecBuilder.propertySpecs.none { it.name == name }) {
                 typeSpecBuilder.addProperty(
                     PropertySpec.builder(name, com.squareup.kotlinpoet.LONG)
                         .addModifiers(KModifier.PRIVATE, KModifier.CONST)
                         .initializer("%L", bitMaskStr)
                         .build()
                 )
             }
         }
 
         for (i in requiredMasks.indices) {
             val reqMask = requiredMasks[i]
             if (reqMask != C.VAL_ZERO_L) {
                 val reqMaskStr = formatMaskString(reqMask)
                 val name = "MASK_REQUIRED_$i"
                 if (typeSpecBuilder.propertySpecs.none { it.name == name }) {
                     typeSpecBuilder.addProperty(
                         PropertySpec.builder(name, com.squareup.kotlinpoet.LONG)
                             .addModifiers(KModifier.PRIVATE, KModifier.CONST)
                             .initializer("%L", reqMaskStr)
                             .build()
                     )
                 }
             }
         }
     }
 
     /**
      * Registers a [MASK_DEFAULTS_N] constant when the copy-based default-value return path needs it.
      */
     protected fun emitDefaultMaskConstant(
         typeSpecBuilder: TypeSpec.Builder,
         maskIndex: Int,
     ): String {
         val defMask = defaultMasks[maskIndex]
         val constName = "${C.STR_MASK_DEFAULTS_PREFIX}$maskIndex"
         if (defMask != C.VAL_ZERO_L &&
             typeSpecBuilder.propertySpecs.none { it.name == constName }
         ) {
             typeSpecBuilder.addProperty(
                 PropertySpec.builder(constName, com.squareup.kotlinpoet.LONG)
                     .addModifiers(KModifier.PRIVATE, KModifier.CONST)
                     .initializer("%L", formatMaskString(defMask))
                     .build()
             )
         }
         return constName
     }
 
     /**
      * Injects private property references for contextual serializers into the generated class.
      *
      * This implements a static Dependency Injection strategy. Instead of looking up
      * serializers at runtime (which would require reflection), we pre-resolve and
      * inject them as private final fields during the compilation phase.
      *
      * @param typeSpecBuilder The [TypeSpec.Builder] where the serializer properties will be added.
      */
     fun injectContextualSerializers(typeSpecBuilder: TypeSpec.Builder) {
         val ghostClass = ClassName(
             C.STR_GHOST_PKG,
             C.STR_GHOST_OBJ
         )
 
         contextualSerializers.forEach { (type, name) ->
             val nonNullableType = type.makeNotNullable()
             typeSpecBuilder.addProperty(
                 PropertySpec.builder(
                     name,
                     ClassName(
                         C.STR_CONTRACT_PKG,
                         C.STR_GHOST_SERIALIZER
                     )
                         .parameterizedBy(nonNullableType.toTypeName()),
                     KModifier.PRIVATE
                 )
                     .initializer(
                         C.TEMPLATE_RESOLVE_SERIALIZER,
                         ghostClass,
                         nonNullableType.toTypeName()
                     )
                     .build()
             )
         }
     }
 
     /**
      * Wraps a deserialization reader instruction with a null-check condition.
      *
      * In the generated code, this wraps the reader call with a check:
      * `if (reader.isNextNullValue()) { reader.consumeNull(); null } else { ... }`
      */
     protected fun nullGuarded(inner: CodeBlock): CodeBlock =
         CodeBlock.of(C.TEMPLATE_NULL_CHECK_L, inner)
 
     private fun isValueClassType(type: KSType): Boolean {
         val declaration = type.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration ?: return false
         return declaration.modifiers.contains(com.google.devtools.ksp.symbol.Modifier.VALUE) ||
                 declaration.modifiers.contains(com.google.devtools.ksp.symbol.Modifier.INLINE)
     }
 
     private fun resolveValueClassInnerType(type: KSType): KSType? {
         val declaration = type.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration ?: return null
         val primaryConstructor = declaration.primaryConstructor ?: return null
         val param = primaryConstructor.parameters.firstOrNull() ?: return null
         return param.type.resolve()
     }
 }