Skip to main content

sqlite_graphrag/config/
settings.rs

1//! Read and write of operational settings (`config get/set/unset/list`).
2//!
3//! The precedence itself (flag > XDG > default) is applied by callers through
4//! [`crate::runtime_config`]; this module only owns the XDG layer.
5
6use super::registry::{is_known_setting, nearest_setting_key, LEGACY_SETTING_KEYS, SETTING_KEYS};
7use super::store::{load_config, save_config};
8use crate::errors::AppError;
9use crate::i18n::validation;
10
11/// Read an operational setting from XDG config (flag > XDG > default is
12/// applied by callers). Returns `None` when unset.
13pub fn get_setting(key: &str) -> Result<Option<String>, AppError> {
14    let cfg = load_config()?;
15    if let Some(v) = cfg.settings.get(key) {
16        return Ok(Some(v.clone()));
17    }
18    // GAP-SG-122: when the canonical key is missing, fall back to any retired
19    // alias that still maps onto it (e.g. paths.cache → cache.dir).
20    for (legacy, replacement) in LEGACY_SETTING_KEYS {
21        if *replacement == key {
22            if let Some(v) = cfg.settings.get(*legacy) {
23                return Ok(Some(v.clone()));
24            }
25        }
26    }
27    Ok(None)
28}
29
30/// Persist an operational setting in XDG config.toml (G-T-XDG-01).
31pub fn set_setting(key: &str, value: &str) -> Result<(), AppError> {
32    if key.trim().is_empty() {
33        return Err(AppError::Validation("config key must be non-empty".into()));
34    }
35    // GAP-SG-80: an unvalidated insert persisted typos and obsolete keys while
36    // reporting success, so the operator saw the value in `config show` and
37    // assumed it took effect. Reject early instead of storing a silent no-op.
38    if !is_known_setting(key) {
39        if let Some(replacement) = LEGACY_SETTING_KEYS
40            .iter()
41            .find(|(legacy, _)| *legacy == key)
42            .map(|(_, replacement)| *replacement)
43        {
44            return Err(AppError::Validation(validation::config_key_retired(
45                key,
46                replacement,
47            )));
48        }
49        return Err(AppError::Validation(validation::config_key_unknown(
50            key,
51            nearest_setting_key(key),
52        )));
53    }
54    // GAP-SG-201: the key was validated and the VALUE never was, so
55    // `embedding.dim nao-numero`, `log.format xml` and `llm.slot_no_wait talvez`
56    // all persisted with exit 0 and only misbehaved on a later invocation, far
57    // from the command that caused them. `display.tz 0` was the extreme case:
58    // it bricked every subsequent run, including the `config unset` that would
59    // have undone it (GAP-SG-200).
60    //
61    // Rejecting here puts the error where the operator can act on it. Keys of
62    // kind `Text` have no checkable domain and skip this by construction.
63    if let Some(entry) = SETTING_KEYS.iter().find(|entry| entry.key == key) {
64        if let Some(expectation) = entry.kind.expectation() {
65            if !entry.kind.accepts(value) {
66                return Err(AppError::Validation(validation::config_value_invalid(
67                    key,
68                    value,
69                    &expectation,
70                )));
71            }
72        }
73    }
74    let mut cfg = load_config()?;
75    cfg.settings.insert(key.to_string(), value.to_string());
76    save_config(&cfg)
77}
78
79/// Remove an operational setting from XDG config.toml.
80pub fn unset_setting(key: &str) -> Result<bool, AppError> {
81    let mut cfg = load_config()?;
82    let removed = cfg.settings.remove(key).is_some();
83    if removed {
84        save_config(&cfg)?;
85    }
86    Ok(removed)
87}
88
89/// List all operational settings (no secrets).
90pub fn list_settings() -> Result<std::collections::BTreeMap<String, String>, AppError> {
91    let cfg = load_config()?;
92    Ok(cfg.settings)
93}