Skip to main content

platform_core/runtime_config/
provider.rs

1use crate::runtime_config::descriptor::RuntimeConfigRegistry;
2use crate::runtime_config::snapshot::RuntimeConfigSnapshot;
3use arc_swap::ArcSwap;
4use std::collections::BTreeMap;
5use std::fmt::Debug;
6use std::sync::Arc;
7
8/// Read access to the current effective configuration for this service.
9///
10/// Reads are cheap and never touch the database; they read the in-memory
11/// snapshot maintained by the implementation.
12pub trait RuntimeConfigProvider: Debug + Send + Sync {
13    /// The current effective snapshot.
14    fn snapshot(&self) -> Arc<RuntimeConfigSnapshot>;
15}
16
17/// Defaults-only provider for tests, the migrate app, and any context without a
18/// database-backed configuration source.
19#[derive(Debug)]
20pub struct StaticRuntimeConfigProvider {
21    snapshot: Arc<RuntimeConfigSnapshot>,
22}
23
24impl StaticRuntimeConfigProvider {
25    /// Resolve a snapshot for `service_key` from registry defaults (no overrides).
26    #[must_use]
27    pub fn new(registry: &RuntimeConfigRegistry, service_key: &str) -> Self {
28        let snapshot = RuntimeConfigSnapshot::resolve(registry, service_key, &BTreeMap::new());
29        Self {
30            snapshot: Arc::new(snapshot),
31        }
32    }
33
34    /// An empty provider (no registered config values) for minimal test contexts.
35    #[must_use]
36    pub fn empty() -> Self {
37        Self {
38            snapshot: Arc::new(RuntimeConfigSnapshot::default()),
39        }
40    }
41}
42
43impl RuntimeConfigProvider for StaticRuntimeConfigProvider {
44    fn snapshot(&self) -> Arc<RuntimeConfigSnapshot> {
45        Arc::clone(&self.snapshot)
46    }
47}
48
49/// Shared, atomically swappable snapshot cell used by the Postgres provider and
50/// its background refresh task (Task 5).
51#[derive(Debug)]
52pub struct RuntimeConfigCell {
53    inner: ArcSwap<RuntimeConfigSnapshot>,
54}
55
56impl RuntimeConfigCell {
57    #[must_use]
58    pub fn new(initial: RuntimeConfigSnapshot) -> Self {
59        Self {
60            inner: ArcSwap::from_pointee(initial),
61        }
62    }
63
64    #[must_use]
65    pub fn load(&self) -> Arc<RuntimeConfigSnapshot> {
66        self.inner.load_full()
67    }
68
69    pub fn store(&self, snapshot: RuntimeConfigSnapshot) {
70        self.inner.store(Arc::new(snapshot));
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::runtime_config::descriptor::{
78        RuntimeConfigDescriptor, RuntimeConfigScope, RuntimeConfigType,
79    };
80    use serde_json::json;
81
82    #[test]
83    fn static_provider_serves_defaults() {
84        let registry = RuntimeConfigRegistry::try_new(vec![RuntimeConfigDescriptor {
85            key: "demo.enabled".to_owned(),
86            scope: RuntimeConfigScope::Shared,
87            group: None,
88            section: None,
89            order: 0,
90            visible_when: None,
91            generated: None,
92            value_type: RuntimeConfigType::Bool,
93            default: json!(true),
94            editable: true,
95            restart_only: false,
96            description: "flag",
97        }])
98        .unwrap();
99        let provider = StaticRuntimeConfigProvider::new(&registry, "api");
100        assert_eq!(provider.snapshot().raw("demo.enabled"), Some(&json!(true)));
101    }
102
103    #[test]
104    fn snapshot_cell_swaps() {
105        let cell = RuntimeConfigCell::new(RuntimeConfigSnapshot::default());
106        assert!(cell.load().raw("x").is_none());
107        let registry = RuntimeConfigRegistry::try_new(vec![RuntimeConfigDescriptor {
108            key: "x".to_owned(),
109            scope: RuntimeConfigScope::Shared,
110            group: None,
111            section: None,
112            order: 0,
113            visible_when: None,
114            generated: None,
115            value_type: RuntimeConfigType::Bool,
116            default: json!(false),
117            editable: true,
118            restart_only: false,
119            description: "x",
120        }])
121        .unwrap();
122        cell.store(RuntimeConfigSnapshot::resolve(
123            &registry,
124            "api",
125            &BTreeMap::new(),
126        ));
127        assert_eq!(cell.load().raw("x"), Some(&json!(false)));
128    }
129}