1use std::{env, ffi::OsStr, fs, path::PathBuf};
2
3use anyhow::{anyhow, Context, Result};
4use serde::Deserialize;
5
6#[derive(Debug, Clone)]
7pub struct Config {
8 pub adr_dir: PathBuf,
9 pub index_name: String,
10 pub template: Option<PathBuf>,
11 pub format: String, pub front_matter: bool, }
14
15impl Default for Config {
16 fn default() -> Self {
17 Self {
18 adr_dir: PathBuf::from("docs/adr"),
19 index_name: "index.md".to_string(),
20 template: None,
21 format: "md".to_string(),
22 front_matter: false,
23 }
24 }
25}
26
27#[derive(Deserialize, Debug)]
28struct FileConfig {
29 adr_dir: Option<PathBuf>,
30 index_name: Option<String>,
31 template: Option<PathBuf>,
32 format: Option<String>,
33 front_matter: Option<bool>,
34}
35
36pub fn load_config(cli_path: Option<&PathBuf>) -> Result<Config> {
37 let mut cfg = Config::default();
38
39 let path = if let Some(p) = cli_path {
40 Some(p.clone())
41 } else if let Ok(env_p) = env::var("RADR_CONFIG") {
42 Some(PathBuf::from(env_p))
43 } else {
44 let candidates = [
45 "radr.toml",
46 "radr.yaml",
47 "radr.yml",
48 "radr.json",
49 ".radrrc.toml",
50 ".radrrc.yaml",
51 ".radrrc.yml",
52 ".radrrc.json",
53 ];
54 candidates.iter().map(PathBuf::from).find(|p| p.exists())
55 };
56
57 if let Some(p) = path {
58 let ext = p.extension().and_then(OsStr::to_str).unwrap_or("");
59 let contents =
60 fs::read_to_string(&p).with_context(|| format!("Reading config at {}", p.display()))?;
61 let fc: FileConfig = match ext.to_ascii_lowercase().as_str() {
62 "json" => serde_json::from_str(&contents)
63 .with_context(|| format!("Parsing JSON config at {}", p.display()))?,
64 "yaml" | "yml" => serde_yaml::from_str(&contents)
65 .with_context(|| format!("Parsing YAML config at {}", p.display()))?,
66 "toml" => toml::from_str(&contents)
67 .with_context(|| format!("Parsing TOML config at {}", p.display()))?,
68 other => return Err(anyhow!("Unsupported config extension: {}", other)),
69 };
70
71 if let Some(d) = fc.adr_dir {
72 cfg.adr_dir = d;
73 }
74 if let Some(i) = fc.index_name {
75 cfg.index_name = i;
76 }
77 if let Some(t) = fc.template {
78 cfg.template = Some(t);
79 }
80 if let Some(f) = fc.format {
81 let f = f.to_ascii_lowercase();
82 if f == "md" || f == "mdx" {
83 cfg.format = f;
84 }
85 }
86 if let Some(fm) = fc.front_matter {
87 cfg.front_matter = fm;
88 }
89 }
90
91 Ok(cfg)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::io::Write;
98 use tempfile::tempdir;
99
100 #[test]
101 fn test_default_config() {
102 let c = Config::default();
103 assert_eq!(c.index_name, "index.md");
104 }
105
106 #[test]
107 fn test_load_from_toml() {
108 let dir = tempdir().unwrap();
109 let path = dir.path().join("radr.toml");
110 let mut f = std::fs::File::create(&path).unwrap();
111 writeln!(f, "adr_dir='adrs'\nindex_name='IDX.md'").unwrap();
112 std::env::set_current_dir(dir.path()).unwrap();
113 let cfg = load_config(None).unwrap();
114 assert_eq!(cfg.adr_dir, PathBuf::from("adrs"));
115 assert_eq!(cfg.index_name, "IDX.md");
116 }
117
118 #[test]
119 fn test_cli_over_env_precedence_and_template() {
120 let dir = tempdir().unwrap();
121 let json = dir.path().join("radr.json");
122 let yaml = dir.path().join("radr.yaml");
123 let tpl = dir.path().join("tpl.md");
124 std::fs::write(&tpl, "T").unwrap();
125 std::fs::write(&json, b"{\n \"adr_dir\": \"cli_adrs\",\n \"index_name\": \"CLI.md\",\n \"template\": \"tpl.md\"\n}\n").unwrap();
126 std::fs::write(&yaml, b"adr_dir: env_adrs\nindex_name: ENV.md\n").unwrap();
127 std::env::set_var("RADR_CONFIG", &yaml);
129 let cfg = load_config(Some(&json)).unwrap();
130 assert_eq!(cfg.adr_dir, PathBuf::from("cli_adrs"));
131 assert_eq!(cfg.index_name, "CLI.md");
132 assert_eq!(
133 cfg.template.as_deref(),
134 Some(PathBuf::from("tpl.md").as_path())
135 );
136 assert_eq!(cfg.format, "md");
138 assert!(!cfg.front_matter);
139 std::env::remove_var("RADR_CONFIG");
140 }
141
142 #[test]
143 fn test_unsupported_extension_errors() {
144 let dir = tempdir().unwrap();
145 let bad = dir.path().join("radr.txt");
146 std::fs::write(&bad, "adr_dir=adrs").unwrap();
147 let err = load_config(Some(&bad)).unwrap_err();
148 let msg = format!("{}", err);
149 assert!(msg.contains("Unsupported config extension"));
150 }
151
152 #[test]
153 fn test_env_over_local_and_defaults() {
154 let dir = tempdir().unwrap();
155 let toml_path = dir.path().join("radr.toml");
157 let mut f = std::fs::File::create(&toml_path).unwrap();
158 writeln!(f, "adr_dir='local'\nindex_name='LOCAL.md'").unwrap();
159 let yaml_path = dir.path().join("radr.yaml");
161 std::fs::write(&yaml_path, b"adr_dir: env\nindex_name: ENV.md\n").unwrap();
162 let d = Config::default();
164 assert_eq!(d.adr_dir, PathBuf::from("docs/adr"));
165 assert_eq!(d.index_name, "index.md");
166 std::env::set_current_dir(dir.path()).unwrap();
168 std::env::set_var("RADR_CONFIG", yaml_path.to_str().unwrap());
169 let cfg = load_config(None).unwrap();
170 assert_eq!(cfg.adr_dir, PathBuf::from("env"));
171 assert_eq!(cfg.index_name, "ENV.md");
172 std::env::remove_var("RADR_CONFIG");
173 }
174
175 #[test]
176 fn test_invalid_config_content_errors() {
177 let dir = tempdir().unwrap();
178 let bad_toml = dir.path().join("radr.toml");
179 std::fs::write(&bad_toml, "adr_dir 'oops'").unwrap();
181 let err = load_config(Some(&bad_toml)).unwrap_err();
182 let msg = format!("{}", err);
183 assert!(msg.contains("Parsing TOML config"));
184 }
185}