1use crate::error::{GuardError, Result};
4use serde::Deserialize;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Deserialize)]
8pub struct Config {
9 #[serde(default = "default_git")]
10 pub git: String,
11 #[serde(default = "default_base")]
12 pub base_ref: String,
13 #[serde(default = "default_globs")]
14 pub reference_globs: Vec<String>,
15 #[serde(rename = "dir", default)]
16 pub dirs: Vec<DirSpec>,
17}
18
19#[derive(Debug, Deserialize, Clone)]
20pub struct DirSpec {
21 pub role: String,
22 pub path: PathBuf,
23}
24
25fn default_git() -> String {
26 "git".into()
27}
28fn default_base() -> String {
29 "main".into()
30}
31fn default_globs() -> Vec<String> {
32 vec!["src/**/*.rs".into()]
33}
34
35impl Config {
36 pub fn load(root: &Path) -> Result<Config> {
38 let path = root.join("migrate-guard.toml");
39 let text = std::fs::read_to_string(&path)
40 .map_err(|e| GuardError::Config(format!("reading {}: {e}", path.display())))?;
41 let cfg: Config = toml::from_str(&text)
42 .map_err(|e| GuardError::Config(format!("parsing {}: {e}", path.display())))?;
43 if cfg.dirs.is_empty() {
44 return Err(GuardError::Config("no [[dir]] entries".into()));
45 }
46 Ok(cfg)
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn parses_full_config() {
56 let tmp = tempfile::tempdir().unwrap();
57 std::fs::write(
58 tmp.path().join("migrate-guard.toml"),
59 r#"
60base_ref = "trunk"
61[[dir]]
62role = "platform"
63path = "backend/migrations"
64[[dir]]
65role = "tenant"
66path = "crates/fastyoke-kernel/migrations_tenant"
67"#,
68 )
69 .unwrap();
70 let cfg = Config::load(tmp.path()).unwrap();
71 assert_eq!(cfg.base_ref, "trunk");
72 assert_eq!(cfg.git, "git"); assert_eq!(cfg.dirs.len(), 2);
74 assert_eq!(cfg.dirs[0].role, "platform");
75 }
76
77 #[test]
78 fn rejects_no_dirs() {
79 let tmp = tempfile::tempdir().unwrap();
80 std::fs::write(
81 tmp.path().join("migrate-guard.toml"),
82 "base_ref = \"main\"\n",
83 )
84 .unwrap();
85 assert!(Config::load(tmp.path()).is_err());
86 }
87}