Skip to main content

oxihuman_core/
config_manager.rs

1//! Configuration manager with profiles and layered overrides.
2
3use std::collections::HashMap;
4
5// ---------------------------------------------------------------------------
6// Types
7// ---------------------------------------------------------------------------
8
9/// A typed configuration value.
10#[allow(dead_code)]
11#[derive(Clone, Debug, PartialEq)]
12pub enum ConfigValue {
13    Bool(bool),
14    Int(i64),
15    Float(f64),
16    Str(String),
17}
18
19/// A named configuration profile containing a map of key→value entries.
20#[allow(dead_code)]
21#[derive(Clone, Debug)]
22pub struct ConfigProfile {
23    /// Name of this profile.
24    pub name: String,
25    /// Key → value store.
26    pub values: HashMap<String, ConfigValue>,
27    /// Whether this profile has unsaved changes.
28    pub dirty: bool,
29}
30
31/// Type alias for looking up a value across profiles.
32#[allow(dead_code)]
33pub type ConfigLookup<'a> = Option<&'a ConfigValue>;
34
35/// Top-level configuration manager holding multiple named profiles.
36#[allow(dead_code)]
37pub struct ConfigManager {
38    /// Ordered list of profiles.
39    pub profiles: Vec<ConfigProfile>,
40    /// Name of the currently active profile.
41    pub active: String,
42}
43
44// ---------------------------------------------------------------------------
45// Construction
46// ---------------------------------------------------------------------------
47
48/// Create a new configuration manager with one default profile named "default".
49#[allow(dead_code)]
50pub fn new_config_manager() -> ConfigManager {
51    let default_profile = ConfigProfile {
52        name: "default".to_string(),
53        values: HashMap::new(),
54        dirty: false,
55    };
56    ConfigManager {
57        profiles: vec![default_profile],
58        active: "default".to_string(),
59    }
60}
61
62// ---------------------------------------------------------------------------
63// Profile management
64// ---------------------------------------------------------------------------
65
66/// Create a new empty profile with the given name.
67/// Returns `false` if a profile with that name already exists.
68#[allow(dead_code)]
69pub fn create_profile(mgr: &mut ConfigManager, name: &str) -> bool {
70    if mgr.profiles.iter().any(|p| p.name == name) {
71        return false;
72    }
73    mgr.profiles.push(ConfigProfile {
74        name: name.to_string(),
75        values: HashMap::new(),
76        dirty: false,
77    });
78    true
79}
80
81/// Delete a profile by name.
82/// Returns `false` if not found or it is the active profile.
83#[allow(dead_code)]
84pub fn delete_profile(mgr: &mut ConfigManager, name: &str) -> bool {
85    if mgr.active == name {
86        return false;
87    }
88    if let Some(pos) = mgr.profiles.iter().position(|p| p.name == name) {
89        mgr.profiles.remove(pos);
90        return true;
91    }
92    false
93}
94
95/// Return the name of the currently active profile.
96#[allow(dead_code)]
97pub fn active_profile(mgr: &ConfigManager) -> &str {
98    &mgr.active
99}
100
101/// Switch the active profile to `name`.
102/// Returns `false` if no such profile exists.
103#[allow(dead_code)]
104pub fn switch_profile(mgr: &mut ConfigManager, name: &str) -> bool {
105    if mgr.profiles.iter().any(|p| p.name == name) {
106        mgr.active = name.to_string();
107        return true;
108    }
109    false
110}
111
112/// Return the total number of profiles.
113#[allow(dead_code)]
114pub fn profile_count(mgr: &ConfigManager) -> usize {
115    mgr.profiles.len()
116}
117
118/// Return a list of all profile names.
119#[allow(dead_code)]
120pub fn list_profiles(mgr: &ConfigManager) -> Vec<&str> {
121    mgr.profiles.iter().map(|p| p.name.as_str()).collect()
122}
123
124// ---------------------------------------------------------------------------
125// Value access
126// ---------------------------------------------------------------------------
127
128/// Set a key-value pair in the named profile.
129/// Returns `false` if the profile does not exist.
130#[allow(dead_code)]
131pub fn set_profile_value(
132    mgr: &mut ConfigManager,
133    profile: &str,
134    key: &str,
135    val: ConfigValue,
136) -> bool {
137    if let Some(p) = mgr.profiles.iter_mut().find(|p| p.name == profile) {
138        p.values.insert(key.to_string(), val);
139        p.dirty = true;
140        return true;
141    }
142    false
143}
144
145/// Get a value from the named profile by key.
146#[allow(dead_code)]
147pub fn get_profile_value<'a>(mgr: &'a ConfigManager, profile: &str, key: &str) -> ConfigLookup<'a> {
148    mgr.profiles
149        .iter()
150        .find(|p| p.name == profile)
151        .and_then(|p| p.values.get(key))
152}
153
154/// Get a value from the active profile, falling back to the "default" profile
155/// if the key is not present in the active profile.
156#[allow(dead_code)]
157pub fn get_value_with_fallback<'a>(mgr: &'a ConfigManager, key: &str) -> ConfigLookup<'a> {
158    let active = mgr.active.clone();
159    if let Some(v) = get_profile_value(mgr, &active, key) {
160        return Some(v);
161    }
162    get_profile_value(mgr, "default", key)
163}
164
165// ---------------------------------------------------------------------------
166// Merge and reset
167// ---------------------------------------------------------------------------
168
169/// Merge all key-value pairs from `src` profile into `dst` profile.
170/// Existing keys in `dst` are overwritten.
171/// Returns `false` if either profile does not exist.
172#[allow(dead_code)]
173pub fn merge_profiles(mgr: &mut ConfigManager, src: &str, dst: &str) -> bool {
174    // Collect src values first to avoid borrow issues.
175    let src_values: Option<Vec<(String, ConfigValue)>> =
176        mgr.profiles.iter().find(|p| p.name == src).map(|p| {
177            p.values
178                .iter()
179                .map(|(k, v)| (k.clone(), v.clone()))
180                .collect()
181        });
182
183    if let Some(pairs) = src_values {
184        if let Some(dst_profile) = mgr.profiles.iter_mut().find(|p| p.name == dst) {
185            for (k, v) in pairs {
186                dst_profile.values.insert(k, v);
187            }
188            dst_profile.dirty = true;
189            return true;
190        }
191    }
192    false
193}
194
195/// Reset a profile to empty (remove all key-value pairs).
196/// Returns `false` if the profile does not exist.
197#[allow(dead_code)]
198pub fn reset_profile_to_defaults(mgr: &mut ConfigManager, profile: &str) -> bool {
199    if let Some(p) = mgr.profiles.iter_mut().find(|p| p.name == profile) {
200        p.values.clear();
201        p.dirty = true;
202        return true;
203    }
204    false
205}
206
207// ---------------------------------------------------------------------------
208// Bulk import
209// ---------------------------------------------------------------------------
210
211/// Populate the named profile from a slice of `(key, value)` string pairs.
212/// Values starting with `true`/`false` become `Bool`, digits become `Int`,
213/// digits with `.` become `Float`, otherwise `Str`.
214/// Returns the number of pairs imported, or `None` if the profile was not found.
215#[allow(dead_code)]
216pub fn config_from_pairs(
217    mgr: &mut ConfigManager,
218    profile: &str,
219    pairs: &[(&str, &str)],
220) -> Option<usize> {
221    let profile_exists = mgr.profiles.iter().any(|p| p.name == profile);
222    if !profile_exists {
223        return None;
224    }
225    let mut count = 0;
226    for (k, v) in pairs {
227        let val = parse_config_value(v);
228        set_profile_value(mgr, profile, k, val);
229        count += 1;
230    }
231    Some(count)
232}
233
234fn parse_config_value(s: &str) -> ConfigValue {
235    if s == "true" {
236        return ConfigValue::Bool(true);
237    }
238    if s == "false" {
239        return ConfigValue::Bool(false);
240    }
241    if s.contains('.') {
242        if let Ok(f) = s.parse::<f64>() {
243            return ConfigValue::Float(f);
244        }
245    }
246    if let Ok(i) = s.parse::<i64>() {
247        return ConfigValue::Int(i);
248    }
249    ConfigValue::Str(s.to_string())
250}
251
252// ---------------------------------------------------------------------------
253// JSON serialisation (minimal, no external deps)
254// ---------------------------------------------------------------------------
255
256fn config_value_to_json(v: &ConfigValue) -> String {
257    match v {
258        ConfigValue::Bool(b) => b.to_string(),
259        ConfigValue::Int(i) => i.to_string(),
260        ConfigValue::Float(f) => format!("{f}"),
261        ConfigValue::Str(s) => format!(r#""{s}""#),
262    }
263}
264
265/// Serialise the entire configuration manager state to a compact JSON string.
266#[allow(dead_code)]
267pub fn config_to_json(mgr: &ConfigManager) -> String {
268    let profiles_json: Vec<String> = mgr
269        .profiles
270        .iter()
271        .map(|p| {
272            let entries: Vec<String> = p
273                .values
274                .iter()
275                .map(|(k, v)| format!(r#""{k}":{}"#, config_value_to_json(v)))
276                .collect();
277            format!(
278                r#"{{"name":"{}","dirty":{},"values":{{{}}}}}"#,
279                p.name,
280                p.dirty,
281                entries.join(",")
282            )
283        })
284        .collect();
285
286    format!(
287        r#"{{"active":"{}","profiles":[{}]}}"#,
288        mgr.active,
289        profiles_json.join(",")
290    )
291}
292
293// ---------------------------------------------------------------------------
294// Tests
295// ---------------------------------------------------------------------------
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_new_config_manager_has_default_profile() {
303        let mgr = new_config_manager();
304        assert_eq!(profile_count(&mgr), 1);
305        assert_eq!(active_profile(&mgr), "default");
306    }
307
308    #[test]
309    fn test_create_profile_succeeds() {
310        let mut mgr = new_config_manager();
311        assert!(create_profile(&mut mgr, "production"));
312        assert_eq!(profile_count(&mgr), 2);
313    }
314
315    #[test]
316    fn test_create_profile_duplicate_fails() {
317        let mut mgr = new_config_manager();
318        create_profile(&mut mgr, "dev");
319        assert!(!create_profile(&mut mgr, "dev"));
320        assert_eq!(profile_count(&mgr), 2);
321    }
322
323    #[test]
324    fn test_delete_profile_removes_it() {
325        let mut mgr = new_config_manager();
326        create_profile(&mut mgr, "temp");
327        assert!(delete_profile(&mut mgr, "temp"));
328        assert_eq!(profile_count(&mgr), 1);
329    }
330
331    #[test]
332    fn test_delete_active_profile_fails() {
333        let mut mgr = new_config_manager();
334        assert!(!delete_profile(&mut mgr, "default"));
335    }
336
337    #[test]
338    fn test_switch_profile() {
339        let mut mgr = new_config_manager();
340        create_profile(&mut mgr, "alt");
341        assert!(switch_profile(&mut mgr, "alt"));
342        assert_eq!(active_profile(&mgr), "alt");
343    }
344
345    #[test]
346    fn test_switch_profile_unknown_fails() {
347        let mut mgr = new_config_manager();
348        assert!(!switch_profile(&mut mgr, "ghost"));
349    }
350
351    #[test]
352    fn test_set_and_get_profile_value_bool() {
353        let mut mgr = new_config_manager();
354        set_profile_value(&mut mgr, "default", "show_grid", ConfigValue::Bool(true));
355        let v = get_profile_value(&mgr, "default", "show_grid");
356        assert_eq!(v, Some(&ConfigValue::Bool(true)));
357    }
358
359    #[test]
360    fn test_set_and_get_profile_value_int() {
361        let mut mgr = new_config_manager();
362        set_profile_value(&mut mgr, "default", "max_fps", ConfigValue::Int(60));
363        let v = get_profile_value(&mgr, "default", "max_fps");
364        assert_eq!(v, Some(&ConfigValue::Int(60)));
365    }
366
367    #[test]
368    fn test_set_and_get_profile_value_float() {
369        let mut mgr = new_config_manager();
370        set_profile_value(&mut mgr, "default", "gamma", ConfigValue::Float(2.2));
371        let v = get_profile_value(&mgr, "default", "gamma");
372        assert_eq!(v, Some(&ConfigValue::Float(2.2)));
373    }
374
375    #[test]
376    fn test_set_and_get_profile_value_str() {
377        let mut mgr = new_config_manager();
378        set_profile_value(
379            &mut mgr,
380            "default",
381            "lang",
382            ConfigValue::Str("en".to_string()),
383        );
384        let v = get_profile_value(&mgr, "default", "lang");
385        assert_eq!(v, Some(&ConfigValue::Str("en".to_string())));
386    }
387
388    #[test]
389    fn test_get_value_with_fallback_uses_active() {
390        let mut mgr = new_config_manager();
391        create_profile(&mut mgr, "custom");
392        switch_profile(&mut mgr, "custom");
393        set_profile_value(&mut mgr, "custom", "key", ConfigValue::Int(99));
394        let v = get_value_with_fallback(&mgr, "key");
395        assert_eq!(v, Some(&ConfigValue::Int(99)));
396    }
397
398    #[test]
399    fn test_get_value_with_fallback_falls_to_default() {
400        let mut mgr = new_config_manager();
401        set_profile_value(
402            &mut mgr,
403            "default",
404            "fallback_key",
405            ConfigValue::Bool(false),
406        );
407        create_profile(&mut mgr, "p2");
408        switch_profile(&mut mgr, "p2");
409        let v = get_value_with_fallback(&mgr, "fallback_key");
410        assert_eq!(v, Some(&ConfigValue::Bool(false)));
411    }
412
413    #[test]
414    fn test_merge_profiles() {
415        let mut mgr = new_config_manager();
416        create_profile(&mut mgr, "src");
417        create_profile(&mut mgr, "dst");
418        set_profile_value(&mut mgr, "src", "x", ConfigValue::Int(1));
419        assert!(merge_profiles(&mut mgr, "src", "dst"));
420        assert_eq!(
421            get_profile_value(&mgr, "dst", "x"),
422            Some(&ConfigValue::Int(1))
423        );
424    }
425
426    #[test]
427    fn test_reset_profile_to_defaults() {
428        let mut mgr = new_config_manager();
429        set_profile_value(&mut mgr, "default", "k", ConfigValue::Int(5));
430        assert!(reset_profile_to_defaults(&mut mgr, "default"));
431        assert!(get_profile_value(&mgr, "default", "k").is_none());
432    }
433
434    #[test]
435    fn test_config_from_pairs_parses_types() {
436        let mut mgr = new_config_manager();
437        let pairs = vec![
438            ("flag", "true"),
439            ("count", "10"),
440            ("ratio", "0.5"),
441            ("name", "hello"),
442        ];
443        let n = config_from_pairs(&mut mgr, "default", &pairs);
444        assert_eq!(n, Some(4));
445        assert_eq!(
446            get_profile_value(&mgr, "default", "flag"),
447            Some(&ConfigValue::Bool(true))
448        );
449        assert_eq!(
450            get_profile_value(&mgr, "default", "count"),
451            Some(&ConfigValue::Int(10))
452        );
453    }
454
455    #[test]
456    fn test_list_profiles() {
457        let mut mgr = new_config_manager();
458        create_profile(&mut mgr, "p1");
459        create_profile(&mut mgr, "p2");
460        let names = list_profiles(&mgr);
461        assert_eq!(names.len(), 3);
462        assert!(names.contains(&"default"));
463        assert!(names.contains(&"p1"));
464    }
465
466    #[test]
467    fn test_config_to_json_contains_active() {
468        let mgr = new_config_manager();
469        let json = config_to_json(&mgr);
470        assert!(json.contains("\"active\":\"default\""));
471    }
472
473    #[test]
474    fn test_config_from_pairs_unknown_profile_returns_none() {
475        let mut mgr = new_config_manager();
476        let result = config_from_pairs(&mut mgr, "nonexistent", &[("k", "v")]);
477        assert!(result.is_none());
478    }
479}