1use crate::config::{HostConfig, RoleConfig, RootConfig};
2use anyhow::{Context, Result, bail};
3use std::path::{Path, PathBuf};
4
5pub struct ConfigLoader {
6 base_dir: PathBuf,
7 root: RootConfig,
8}
9
10impl ConfigLoader {
11 pub fn new(base_dir: &Path) -> Result<Self> {
12 let config_path = base_dir.join("dotm.toml");
13 let content = std::fs::read_to_string(&config_path)
14 .with_context(|| format!("failed to read {}", config_path.display()))?;
15 let root: RootConfig = toml::from_str(&content)
16 .with_context(|| format!("failed to parse {}", config_path.display()))?;
17
18 Ok(Self {
19 base_dir: base_dir.to_path_buf(),
20 root,
21 })
22 }
23
24 pub fn root(&self) -> &RootConfig {
25 &self.root
26 }
27
28 pub fn base_dir(&self) -> &Path {
29 &self.base_dir
30 }
31
32 pub fn packages_dir(&self) -> PathBuf {
33 self.base_dir.join(&self.root.dotm.packages_dir)
34 }
35
36 pub fn load_host(&self, hostname: &str) -> Result<HostConfig> {
37 let path = self.base_dir.join("hosts").join(format!("{hostname}.toml"));
38 if !path.exists() {
39 bail!("host config not found: {}", path.display());
40 }
41 let content = std::fs::read_to_string(&path)
42 .with_context(|| format!("failed to read {}", path.display()))?;
43 let config: HostConfig = toml::from_str(&content)
44 .with_context(|| format!("failed to parse {}", path.display()))?;
45 Ok(config)
46 }
47
48 pub fn list_hosts(&self) -> Result<Vec<String>> {
49 let hosts_dir = self.base_dir.join("hosts");
50 if !hosts_dir.is_dir() {
51 return Ok(Vec::new());
52 }
53 let mut names = Vec::new();
54 for entry in std::fs::read_dir(&hosts_dir)? {
55 let entry = entry?;
56 let path = entry.path();
57 if path.extension().and_then(|e| e.to_str()) == Some("toml") {
58 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
59 names.push(stem.to_string());
60 }
61 }
62 }
63 names.sort();
64 Ok(names)
65 }
66
67 pub fn list_roles(&self) -> Result<Vec<String>> {
68 let roles_dir = self.base_dir.join("roles");
69 if !roles_dir.is_dir() {
70 return Ok(Vec::new());
71 }
72 let mut names = Vec::new();
73 for entry in std::fs::read_dir(&roles_dir)? {
74 let entry = entry?;
75 let path = entry.path();
76 if path.extension().and_then(|e| e.to_str()) == Some("toml") {
77 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
78 names.push(stem.to_string());
79 }
80 }
81 }
82 names.sort();
83 Ok(names)
84 }
85
86 pub fn load_role(&self, name: &str) -> Result<RoleConfig> {
87 let path = self.base_dir.join("roles").join(format!("{name}.toml"));
88 if !path.exists() {
89 bail!("role config not found: {}", path.display());
90 }
91 let content = std::fs::read_to_string(&path)
92 .with_context(|| format!("failed to read {}", path.display()))?;
93 let config: RoleConfig = toml::from_str(&content)
94 .with_context(|| format!("failed to parse {}", path.display()))?;
95 Ok(config)
96 }
97}