mdvault_core/config/
loader.rs1use 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
112pub 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}