winbrew_database\config/
mod.rs

1use crate::core::{fs::atomic_write_toml_temp, paths};
2use anyhow::{Context, Result};
3use std::fs;
4use std::path::{Path, PathBuf};
5use tracing::warn;
6
7mod error;
8mod keys;
9mod lookup;
10mod registry;
11mod storage;
12mod types;
13mod validation;
14
15pub use error::{ConfigError, ConfigValidationError};
16pub use storage::{config_sections, config_set, config_unset, get_effective_value};
17pub use types::*;
18
19pub fn suggest_key(key: &str) -> Option<&'static str> {
20    registry::suggest_key(key)
21}
22
23impl Config {
24    pub fn load(path: &Path) -> Result<Self> {
25        match fs::read_to_string(path) {
26            Ok(contents) => {
27                if contents.trim().is_empty() {
28                    Ok(Self::default())
29                } else {
30                    toml::from_str(&contents).context("failed to parse config file")
31                }
32            }
33            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
34            Err(err) => Err(anyhow::Error::new(err).context("failed to read config file")),
35        }
36    }
37
38    pub fn load_at(root: &Path) -> Result<Self> {
39        let mut config = Self::load(&paths::config_file_at(root))?;
40        config.paths.root = root
41            .to_str()
42            .context("config root path is not valid UTF-8")?
43            .to_owned();
44
45        // Explicit-root loads are used by tests and isolated stores, so they
46        // intentionally ignore ambient WINBREW_* overrides.
47        Ok(config.with_env(ConfigEnv::default()).with_config_root(root))
48    }
49
50    pub fn save(&self, path: &Path) -> Result<()> {
51        let contents = toml::to_string_pretty(self).context("failed to serialize config file")?;
52        Ok(atomic_write_toml_temp(path, &contents)?)
53    }
54
55    /// Save the config back to the root it was loaded from.
56    ///
57    /// For [`Config::load_current`], this is the environment/default-selected
58    /// config storage root. For [`Config::load_at`], this remains the explicit
59    /// root passed by the caller, so ambient `WINBREW_*` overrides stay ignored.
60    /// This storage root can differ from the effective runtime `paths.root`.
61    pub fn save_default(&self) -> Result<()> {
62        let root = self.config_storage_root();
63        let config_path = paths::config_file_at(&root);
64        self.save(&config_path)
65    }
66
67    pub fn load_current() -> Result<Self> {
68        let env = ConfigEnv::capture();
69        let root = Self::resolve_root_from_env(&env);
70        let config_path = paths::config_file_at(&root);
71        Ok(Self::load(&config_path)?
72            .with_env(env)
73            .with_config_root(&root))
74    }
75
76    /// Build runtime paths from the effective config values.
77    ///
78    /// This uses effective `paths.root`, including environment overrides. That
79    /// is intentionally separate from the config storage root used by
80    /// [`Config::save_default`].
81    pub fn resolved_paths(&self) -> paths::ResolvedPaths {
82        let root = self.runtime_root();
83        paths::resolved_paths(
84            &root,
85            &self.paths.packages,
86            &self.paths.data,
87            &self.paths.logs,
88            &self.paths.cache,
89        )
90    }
91
92    fn with_env(mut self, env: ConfigEnv) -> Self {
93        self.env = env;
94        self
95    }
96
97    fn with_config_root(mut self, root: &Path) -> Self {
98        self.config_root = Some(root.to_path_buf());
99        self
100    }
101
102    fn config_storage_root(&self) -> PathBuf {
103        self.config_root
104            .clone()
105            .unwrap_or_else(|| Self::resolve_root_from_env(&self.env))
106    }
107
108    fn runtime_root(&self) -> PathBuf {
109        self.effective_value("paths.root")
110            .map(|(value, _)| PathBuf::from(value))
111            .unwrap_or_else(|err| {
112                warn!(
113                    error = %err,
114                    "effective_value(\"paths.root\") failed, using raw config field as fallback"
115                );
116                PathBuf::from(&self.paths.root)
117            })
118    }
119
120    fn resolve_root_from_env(env: &ConfigEnv) -> PathBuf {
121        env.root_override()
122            .map(PathBuf::from)
123            .unwrap_or_else(|| PathBuf::from(default_root_path()))
124    }
125}