1pub mod git_provider;
2pub mod layered;
3pub mod source;
4pub mod types;
5
6pub use layered::{ResolvedConfig, load_layered_config};
7pub use source::{ConfigSource, Sourced};
8pub use types::{Component, Config, Settings, UnmatchedPolicy};
9
10use crate::core::ConfigError;
11use std::path::Path;
12
13pub fn load_config(path: &Path) -> Result<Config, ConfigError> {
15 if !path.exists() {
16 return Err(ConfigError::NotFound {
17 path: path.to_path_buf(),
18 });
19 }
20
21 let content = std::fs::read_to_string(path)?;
22 let config: Config = toml::from_str(&content).map_err(|e| ConfigError::Invalid {
23 reason: e.to_string(),
24 })?;
25
26 validate_config(&config)?;
27 Ok(config)
28}
29
30fn validate_config(config: &Config) -> Result<(), ConfigError> {
32 let mut seen = std::collections::HashSet::new();
34 for component in &config.components {
35 if !seen.insert(&component.name) {
36 return Err(ConfigError::Invalid {
37 reason: format!("duplicate component name: {:?}", component.name),
38 });
39 }
40 for pattern in &component.globs {
41 globset::Glob::new(pattern).map_err(|e| ConfigError::InvalidGlob {
42 component: component.name.clone(),
43 pattern: pattern.clone(),
44 reason: e.to_string(),
45 })?;
46 }
47 }
48 Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use std::io::Write;
55
56 fn write_toml(dir: &std::path::Path, content: &str) -> std::path::PathBuf {
57 let path = dir.join(".atomic.toml");
58 let mut f = std::fs::File::create(&path).unwrap();
59 f.write_all(content.as_bytes()).unwrap();
60 path
61 }
62
63 #[test]
64 fn load_valid_config() {
65 let dir = tempfile::tempdir().unwrap();
66 let path = write_toml(
67 dir.path(),
68 r#"
69[settings]
70base_branch = "develop"
71
72[[components]]
73name = "frontend"
74globs = ["src/ui/**"]
75
76[[components]]
77name = "backend"
78globs = ["src/api/**", "src/db/**"]
79commit_type = "fix"
80"#,
81 );
82
83 let cfg = load_config(&path).unwrap();
84 assert_eq!(cfg.settings.base_branch, "develop");
85 assert_eq!(cfg.settings.unmatched_files, UnmatchedPolicy::Error);
86 assert_eq!(cfg.components.len(), 2);
87
88 assert_eq!(cfg.components[0].name, "frontend");
90 assert_eq!(cfg.components[1].name, "backend");
91 }
92
93 #[test]
94 fn defaults_applied() {
95 let dir = tempfile::tempdir().unwrap();
96 let path = write_toml(
97 dir.path(),
98 r#"
99[[components]]
100name = "app"
101globs = ["**"]
102"#,
103 );
104
105 let cfg = load_config(&path).unwrap();
106 assert_eq!(cfg.settings.base_branch, "main");
107 assert_eq!(cfg.settings.branch_template, "atomic/{component}");
108 assert_eq!(cfg.settings.unmatched_files, UnmatchedPolicy::Error);
109 }
110
111 #[test]
112 fn invalid_glob_rejected() {
113 let dir = tempfile::tempdir().unwrap();
114 let path = write_toml(
115 dir.path(),
116 r#"
117[[components]]
118name = "bad"
119globs = ["[invalid"]
120"#,
121 );
122
123 let err = load_config(&path).unwrap_err();
124 assert!(matches!(err, ConfigError::InvalidGlob { .. }));
125 }
126
127 #[test]
128 fn missing_config_file() {
129 let err = load_config(Path::new("/nonexistent/.atomic.toml")).unwrap_err();
130 assert!(matches!(err, ConfigError::NotFound { .. }));
131 }
132
133 #[test]
134 fn duplicate_component_names_rejected() {
135 let dir = tempfile::tempdir().unwrap();
136 let path = write_toml(
137 dir.path(),
138 r#"
139[[components]]
140name = "app"
141globs = ["src/**"]
142
143[[components]]
144name = "app"
145globs = ["lib/**"]
146"#,
147 );
148
149 let err = load_config(&path).unwrap_err();
150 assert!(matches!(err, ConfigError::Invalid { .. }));
151 }
152}