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

Class Class, % Method, % Branch, % Line, % Instruction, %
ConfigResolution 100% (1/1) 100% (1/1) 100% (4/4) 100% (17/17)


 package com.georgeci.moneysurfer.appconfig
 
 import com.georgeci.moneysurfer.domain.preferences.Pref
 import kotlinx.coroutines.flow.Flow
 
 /**
  * The configuration engine: resolves every value from ordered layers.
  *
  * Infrastructure, not API. Features never see it — they inject typed domain facades
  * (`UiPreferences`, `SyncSettings`, `HostCapabilities`, `DebugConfigInspector`), and `Config` is
  * injected only into those facade implementations, never into a ViewModel. The fence is the
  * dependency graph: feature modules have no `app-config` on its classpath.
  */
 interface Config {
 
     /** First non-absent layer, re-resolved whenever any layer changes. Falls back to `key.default`. */
     fun <T : Any> observe(key: ConfigKey<T>): Flow<T>
 
     /**
      * Synchronous resolution. Requires [hydrate] to have completed: called earlier it resolves the
      * Build layer and defaults only, and throws in debug builds so the ordering mistake is not
      * silently shipped.
      */
     fun <T : Any> snapshot(key: ConfigKey<T>): T
 
     /**
      * Read/write handle for a user setting. Takes [SettingKey] only, so writing to a host- or
      * server-owned flag fails to compile.
      */
     fun <T : Any> handle(key: SettingKey<T>): Pref<T>
 
     /**
      * Per-layer detail for the debug panel. Layers stay honest here — nothing is clamped, including
      * a RemoteGlobal value the `remoteOverridable` opt-in refuses: it is reported as held and simply
      * never wins.
      */
     fun <T : Any> resolve(key: ConfigKey<T>): ConfigResolution<T>
 
     /**
      * Emits once with the current state, then again whenever any layer changes.
      *
      * For consumers that re-resolve *many* keys at once — the debug panel — where per-key
      * [observe] would need one subscription each and would dedupe away the ticks it needs.
      */
     val changes: Flow<Unit>
 
     /**
      * Warms every layer's in-memory mirror. Awaited once at startup, before the first
      * [snapshot]. Idempotent.
      *
      * Never throws: a layer whose store cannot be read is skipped and named in [degradedLayers],
      * because this runs before a start route exists and a failure here would otherwise take the
      * whole app down with it.
      */
     suspend fun hydrate()
 
     /**
      * Layers [hydrate] could not warm. Empty on the happy path.
      *
      * A degraded layer resolves as absent, so values fall through to the layers below and finally to
      * `key.default` — correct, but not necessarily current. Kept observable so the debug panel can
      * distinguish "this layer holds nothing" from "this layer could not be read"; the mirror refills
      * on its own once the store is readable again.
      */
     val degradedLayers: Set<ConfigLayer>
 }
 
 data class ConfigResolution<T : Any>(
     val value: T,
     /** `null` when no layer holds the key and `key.default` won. */
     val winner: ConfigLayer?,
     /**
      * What each layer holds, not what it was allowed to serve. Absent vs undecodable are distinct —
      * the panel must show which — and a value present in a layer that did not win was either
      * outranked or refused by the remote opt-in.
      */
     val perLayer: Map<ConfigLayer, LayerValue<T>>,
 )