mdvault_core/config/
loader.rs

1use crate::config::types::{ConfigFile, Profile, ResolvedConfig, SecurityPolicy};
2use shellexpand::full;
3use std::path::{Path, PathBuf};
4use std::{env, fs};
5
6use dirs::home_dir;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum ConfigError {
11    #[error("config file not found at {0}")]
12    NotFound(String),
13
14    #[error("failed to read config file {0}: {1}")]
15    ReadError(String, #[source] std::io::Error),
16
17    #[error("failed to parse TOML in {0}: {1}")]
18    ParseError(String, #[source] toml::de::Error),
19
20    #[error("profile '{0}' not found")]
21    ProfileNotFound(String),
22
23    #[error("no profiles defined in config")]
24    NoProfiles,
25
26    #[error("version {0} is unsupported (expected 1)")]
27    BadVersion(u32),
28
29    #[error("home directory not available to expand '~'")]
30    NoHome,
31}
32
33pub struct ConfigLoader;
34
35impl ConfigLoader {
36    pub fn load(
37        config_path: Option<&Path>,
38        profile_override: Option<&str>,
39    ) -> Result<ResolvedConfig, ConfigError> {
40        let path = match config_path {
41            Some(p) => p.to_path_buf(),
42            None => default_config_path(),
43        };
44
45        if !path.exists() {
46            return Err(ConfigError::NotFound(path.display().to_string()));
47        }
48
49        let s = fs::read_to_string(&path)
50            .map_err(|e| ConfigError::ReadError(path.display().to_string(), e))?;
51
52        let cf: ConfigFile = toml::from_str(&s)
53            .map_err(|e| ConfigError::ParseError(path.display().to_string(), e))?;
54
55        if cf.version != 1 {
56            return Err(ConfigError::BadVersion(cf.version));
57        }
58        if cf.profiles.is_empty() {
59            return Err(ConfigError::NoProfiles);
60        }
61
62        let active = profile_override
63            .map(ToOwned::to_owned)
64            .or(cf.profile.clone())
65            .unwrap_or_else(|| "default".to_string());
66
67        let prof = cf
68            .profiles
69            .get(&active)
70            .ok_or_else(|| ConfigError::ProfileNotFound(active.clone()))?;
71
72        let resolved = Self::resolve_profile(&active, prof, &cf.security)?;
73        Ok(resolved)
74    }
75
76    fn resolve_profile(
77        active: &str,
78        prof: &Profile,
79        sec: &SecurityPolicy,
80    ) -> Result<ResolvedConfig, ConfigError> {
81        let vault_root = expand_path(&prof.vault_root)?;
82        let sub = |s: &str| s.replace("{{vault_root}}", &vault_root.to_string_lossy());
83
84        let templates_dir = expand_path(&sub(&prof.templates_dir))?;
85        let captures_dir = expand_path(&sub(&prof.captures_dir))?;
86        let macros_dir = expand_path(&sub(&prof.macros_dir))?;
87        let typedefs_dir = match &prof.typedefs_dir {
88            Some(dir) => expand_path(&sub(dir))?,
89            None => default_typedefs_dir(),
90        };
91
92        Ok(ResolvedConfig {
93            active_profile: active.to_string(),
94            vault_root,
95            templates_dir,
96            captures_dir,
97            macros_dir,
98            typedefs_dir,
99            security: sec.clone(),
100        })
101    }
102}
103
104pub fn default_config_path() -> PathBuf {
105    if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
106        return Path::new(&xdg).join("mdvault").join("config.toml");
107    }
108    let home = home_dir().unwrap_or_else(|| PathBuf::from("~"));
109    home.join(".config").join("mdvault").join("config.toml")
110}
111
112/// Default directory for Lua type definitions.
113/// Global location: ~/.config/mdvault/types/
114pub fn default_typedefs_dir() -> PathBuf {
115    if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
116        return Path::new(&xdg).join("mdvault").join("types");
117    }
118    let home = home_dir().unwrap_or_else(|| PathBuf::from("~"));
119    home.join(".config").join("mdvault").join("types")
120}
121
122fn expand_path(input: &str) -> Result<PathBuf, ConfigError> {
123    let expanded = full(input).map_err(|_| ConfigError::NoHome)?;
124    Ok(PathBuf::from(expanded.to_string()))
125}