1#![allow(rustdoc::broken_intra_doc_links)]
2
3use std::path::PathBuf;
4
5use super::model::BhtuneConfig;
6use super::paths::templates_path_from;
7
8pub fn load_user_templates(
26 cli_templates: Option<PathBuf>,
27 config: &BhtuneConfig,
28 xdg_config_home: Option<&str>,
29 home: Option<&str>,
30 appdata: Option<&str>,
31 is_windows: bool,
32) -> anyhow::Result<Option<Vec<bhtune_core::DcsTemplate>>> {
33 let (path, missing_is_error) = match cli_templates.or_else(|| config.templates.clone()) {
34 Some(explicit) => (Some(explicit), true),
35 None => (
36 templates_path_from(xdg_config_home, home, appdata, is_windows),
37 false,
38 ),
39 };
40 let Some(path) = path else {
41 return Ok(None);
42 };
43
44 match std::fs::read_to_string(&path) {
45 Ok(contents) => bhtune_core::template::parse_catalog(&contents)
46 .map(Some)
47 .map_err(|e| anyhow::anyhow!("failed to parse templates file {}: {e}", path.display())),
48 Err(e) if e.kind() == std::io::ErrorKind::NotFound && !missing_is_error => Ok(None),
49 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(anyhow::anyhow!(
50 "templates file not found: {}",
51 path.display()
52 )),
53 Err(e) => Err(anyhow::anyhow!(
54 "failed to read templates file {}: {e}",
55 path.display()
56 )),
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use std::io::Write;
64 fn valid_templates_toml(name: &str) -> String {
68 format!(
69 r#"
70[[template]]
71name = "{name}"
72revert_mode = false
73proportional_type = "band"
74integral_type = "reset_time"
75integral_unit = "seconds"
76derivative_type = "derivative_time"
77derivative_unit = "seconds"
78pid_rounding = {{ kind = "significant_digits", digits = 3 }}
79process_variable_suffix = "PV"
80manipulated_variable_suffix = "MV"
81setpoint_variable_suffix = "SV"
82controller_direction_suffix = ""
83controller_mode_suffix = ""
84mode_attribute_suffix = ""
85upper_pv_range_suffix = "SH"
86lower_pv_range_suffix = "SL"
87upper_mv_range_suffix = "MSH"
88lower_mv_range_suffix = "MSL"
89proportional_constant_suffix = "P"
90integral_constant_suffix = "I"
91derivative_constant_suffix = "D"
92mode_manual_value = ""
93mode_auto_value = ""
94controller_action_direct_value = "0"
95"#
96 )
97 }
98
99 #[test]
100 fn load_user_templates_nothing_explicit_and_nothing_discovered_returns_none() {
101 let templates =
102 load_user_templates(None, &BhtuneConfig::default(), None, None, None, false).unwrap();
103 assert_eq!(templates, None);
104 }
105
106 #[test]
107 fn load_user_templates_auto_discovered_path_missing_is_not_an_error() {
108 let dir = tempfile::tempdir().unwrap();
113 let templates = load_user_templates(
114 None,
115 &BhtuneConfig::default(),
116 Some(dir.path().to_str().unwrap()),
117 None,
118 None,
119 false,
120 )
121 .unwrap();
122 assert_eq!(templates, None);
123 }
124
125 #[test]
126 fn load_user_templates_explicit_cli_path_missing_is_an_error() {
127 let err = load_user_templates(
128 Some(PathBuf::from("/nonexistent/templates.toml")),
129 &BhtuneConfig::default(),
130 None,
131 None,
132 None,
133 false,
134 )
135 .unwrap_err();
136 assert!(err.to_string().contains("templates file not found"));
137 }
138
139 #[test]
140 fn load_user_templates_explicit_config_key_path_missing_is_an_error() {
141 let config = BhtuneConfig {
142 templates: Some(PathBuf::from("/nonexistent/templates.toml")),
143 ..Default::default()
144 };
145 let err = load_user_templates(None, &config, None, None, None, false).unwrap_err();
146 assert!(err.to_string().contains("templates file not found"));
147 }
148
149 #[test]
150 fn load_user_templates_valid_file_is_parsed_and_validated() {
151 let mut file = tempfile::NamedTempFile::new().unwrap();
152 write!(file, "{}", valid_templates_toml("Test Template")).unwrap();
153 let templates = load_user_templates(
154 Some(file.path().to_path_buf()),
155 &BhtuneConfig::default(),
156 None,
157 None,
158 None,
159 false,
160 )
161 .unwrap()
162 .unwrap();
163 assert_eq!(templates.len(), 1);
164 assert_eq!(templates[0].name, "Test Template");
165 }
166
167 #[test]
168 fn load_user_templates_malformed_toml_is_an_error_naming_the_file() {
169 let mut file = tempfile::NamedTempFile::new().unwrap();
170 writeln!(file, "this is not valid toml [[[").unwrap();
171 let err = load_user_templates(
172 Some(file.path().to_path_buf()),
173 &BhtuneConfig::default(),
174 None,
175 None,
176 None,
177 false,
178 )
179 .unwrap_err();
180 let message = err.to_string();
181 assert!(message.contains("failed to parse templates file"));
182 assert!(message.contains(&file.path().display().to_string()));
183 }
184
185 #[test]
186 fn load_user_templates_a_template_failing_validation_is_an_error() {
187 let mut file = tempfile::NamedTempFile::new().unwrap();
191 let toml = valid_templates_toml("Broken Template").replace(
192 "manipulated_variable_suffix = \"MV\"",
193 "manipulated_variable_suffix = \"\"",
194 );
195 write!(file, "{toml}").unwrap();
196 let err = load_user_templates(
197 Some(file.path().to_path_buf()),
198 &BhtuneConfig::default(),
199 None,
200 None,
201 None,
202 false,
203 )
204 .unwrap_err();
205 assert!(
206 err.to_string()
207 .contains("manipulated_variable_suffix must not be empty")
208 );
209 }
210
211 #[test]
212 fn load_user_templates_generic_io_error_is_an_error() {
213 let dir = tempfile::tempdir().unwrap();
217 let err = load_user_templates(
218 Some(dir.path().to_path_buf()),
219 &BhtuneConfig::default(),
220 None,
221 None,
222 None,
223 false,
224 )
225 .unwrap_err();
226 assert!(err.to_string().contains("failed to read templates file"));
227 }
228
229 #[test]
230 fn load_user_templates_cli_flag_wins_over_config_key() {
231 let mut cli_file = tempfile::NamedTempFile::new().unwrap();
232 write!(cli_file, "{}", valid_templates_toml("From CLI Flag")).unwrap();
233 let mut config_file = tempfile::NamedTempFile::new().unwrap();
234 write!(config_file, "{}", valid_templates_toml("From Config File")).unwrap();
235
236 let config = BhtuneConfig {
237 templates: Some(config_file.path().to_path_buf()),
238 ..Default::default()
239 };
240 let templates = load_user_templates(
241 Some(cli_file.path().to_path_buf()),
242 &config,
243 None,
244 None,
245 None,
246 false,
247 )
248 .unwrap()
249 .unwrap();
250 assert_eq!(templates[0].name, "From CLI Flag");
251 }
252
253 #[test]
254 fn load_user_templates_config_key_is_used_when_no_cli_flag_is_given() {
255 let mut config_file = tempfile::NamedTempFile::new().unwrap();
256 write!(config_file, "{}", valid_templates_toml("From Config File")).unwrap();
257 let config = BhtuneConfig {
258 templates: Some(config_file.path().to_path_buf()),
259 ..Default::default()
260 };
261 let templates = load_user_templates(None, &config, None, None, None, false)
262 .unwrap()
263 .unwrap();
264 assert_eq!(templates[0].name, "From Config File");
265 }
266}