Coverage Summary for Class: DebugConfigInspectorImpl (com.georgeci.moneysurfer.appconfig)

Class Method, % Branch, % Line, % Instruction, %
DebugConfigInspectorImpl 77.8% (7/9) 81.8% (18/22) 92.3% (36/39) 92.8% (271/292)
DebugConfigInspectorImpl$Companion
DebugConfigInspectorImpl$override$1
DebugConfigInspectorImpl$special$$inlined$map$1 0% (0/2)
DebugConfigInspectorImpl$special$$inlined$map$1$1
DebugConfigInspectorImpl$special$$inlined$map$1$2 0% (0/1)
DebugConfigInspectorImpl$special$$inlined$map$1$2$1
Total 58.3% (7/12) 81.8% (18/22) 92.3% (36/39) 92.8% (271/292)


 package com.georgeci.moneysurfer.appconfig
 
 import com.georgeci.moneysurfer.domain.config.ConfigDebugLayerCell
 import com.georgeci.moneysurfer.domain.config.ConfigDebugRow
 import com.georgeci.moneysurfer.domain.config.ConfigDebugRowKind
 import com.georgeci.moneysurfer.domain.config.DebugConfigInspector
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import org.koin.core.annotation.Single
 
 /**
  * Renders the whole registry for the QA panel and takes string-keyed writes back.
  *
  * Nothing typed crosses into `feature/settings`: rows are pre-rendered strings, and an override
  * arrives as `(name, raw)` and is parsed through the codec the registry resolves for that name.
  */
 @Single(binds = [DebugConfigInspector::class])
 class DebugConfigInspectorImpl(
     private val config: Config,
     private val registry: ConfigRegistry,
     private val debugSource: DebugConfigSource,
 ) : DebugConfigInspector {
 
     override val isAvailable: Boolean get() = debugSource.isActive
 
     override val degradedLayers: List<String> get() = config.degradedLayers.map { it.name }
 
     override val rows: Flow<List<ConfigDebugRow>> = config.changes
         .map { registry.keys.map { key -> key.toRow() } }
         .distinctUntilChanged()
 
     override suspend fun override(name: String, raw: String): Result<Unit> {
         val key = registry.find(name)
             ?: return Result.failure(IllegalArgumentException("Unknown configuration key '$name'"))
         if (key.codec.decode(raw) == null) {
             return Result.failure(IllegalArgumentException("'$raw' is not a valid value for '$name'"))
         }
         debugSource.override(key, raw)
         return Result.success(Unit)
     }
 
     override suspend fun clearOverride(name: String) {
         registry.find(name)?.let { debugSource.clear(it) }
     }
 
     override suspend fun resetAll() = debugSource.clearAll()
 
     private fun <T : Any> ConfigKey<T>.toRow(): ConfigDebugRow {
         val resolution = config.resolve(this)
         return ConfigDebugRow(
             name = name,
             effectiveValue = codec.encode(resolution.value),
             winner = resolution.winner?.name ?: DEFAULT_WINNER,
             kind = codec.valueKind.toRowKind(),
             // Anything stored in the Debug layer counts, not just a value that won: an override the
             // codec can no longer read does not win, and deriving this from the winner left it
             // unclearable per row — "Reset all" was the only way out, taking every other override
             // with it.
             overridden = resolution.perLayer[ConfigLayer.Debug].let {
                 it != null && it != LayerValue.Absent
             },
             hostOwned = this in HostConfigKeys.all,
             layers = ConfigLayer.entries.map { layer ->
                 resolution.perLayer[layer].toCell(layer, this)
             },
         )
     }
 
     private fun <T : Any> LayerValue<T>?.toCell(layer: ConfigLayer, key: ConfigKey<T>): ConfigDebugLayerCell =
         when (this) {
             null, LayerValue.Absent -> ConfigDebugLayerCell(layer.name, value = null, undecodable = false)
             is LayerValue.Undecodable -> ConfigDebugLayerCell(layer.name, value = raw, undecodable = true)
             is LayerValue.Present -> ConfigDebugLayerCell(
                 layer = layer.name,
                 value = key.codec.encode(value),
                 undecodable = false,
             )
         }
 
     private fun ConfigValueKind.toRowKind(): ConfigDebugRowKind = when (this) {
         ConfigValueKind.Bool -> ConfigDebugRowKind.Bool
         is ConfigValueKind.Choice -> ConfigDebugRowKind.Choice(choices)
         ConfigValueKind.FreeText -> ConfigDebugRowKind.FreeText
     }
 
     private companion object {
         const val DEFAULT_WINNER = "default"
     }
 }