1use indexmap::IndexMap;
3use std::sync::Arc;
4use std::{fmt, io};
5
6mod file;
7mod parser;
8
9use file::{File, SyncFile};
10
11#[derive(Debug)]
12pub struct ParsingError {
13 pub core: u64,
14 pub cause: String,
15}
16
17impl From<io::Error> for ParsingError {
18 fn from(value: io::Error) -> Self {
19 ParsingError {
20 core: 1,
21 cause: format!("{value}"),
22 }
23 }
24}
25
26impl std::fmt::Display for ParsingError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "{}", self.cause)
29 }
30}
31
32pub trait SyncCLNConf {
33 fn parse(&mut self) -> Result<(), ParsingError>;
34}
35
36#[derive(Debug, Clone)]
40pub struct CLNConf {
41 pub fields: IndexMap<String, Vec<String>>,
47 pub includes: Vec<Arc<CLNConf>>,
49 pub path: String,
50 create_if_missing: bool,
51}
52
53impl CLNConf {
54 pub fn new(path: String, create_if_missing: bool) -> Self {
57 CLNConf {
58 fields: IndexMap::new(),
59 includes: Vec::new(),
60 path,
61 create_if_missing,
62 }
63 }
64
65 pub fn parser(&self) -> parser::Parser {
67 parser::Parser::new(&self.path, self.create_if_missing)
68 }
69
70 pub fn add_conf(&mut self, key: &str, val: &str) -> Result<(), ParsingError> {
71 if self.fields.contains_key(key) {
72 let values = self.fields.get_mut(key).unwrap();
73 for value in values.iter() {
74 if val == value {
75 return Err(ParsingError {
76 core: 2,
77 cause: format!("field {key} with value {val} already present"),
78 });
79 }
80 }
81 values.push(val.to_owned());
82 } else {
83 self.fields.insert(key.to_owned(), vec![val.to_owned()]);
84 }
85 Ok(())
86 }
87
88 pub fn get_conf(&self, key: &str) -> Result<Option<String>, ParsingError> {
93 let mut results = vec![];
94 if let Some(fields) = self.fields.get(key) {
95 results.append(&mut fields.clone());
96 }
97 for include in &self.includes {
98 let fields = include.get_confs(key);
99 if !fields.is_empty() {
100 results.append(&mut fields.clone());
101 }
102 }
103 if results.is_empty() {
104 return Ok(None);
105 }
106
107 if results.len() > 1 {
108 return Err(ParsingError {
109 core: 1,
110 cause: "mutiple field with the `{key}`".to_owned(),
111 });
112 }
113 Ok(Some(results.first().unwrap().clone()))
114 }
115
116 pub fn get_confs(&self, key: &str) -> Vec<String> {
119 let mut results = vec![];
120 if let Some(fields) = self.fields.get(key) {
121 results.append(&mut fields.clone());
122 }
123 for include in &self.includes {
124 let fields = include.get_confs(key);
125 if !fields.is_empty() {
126 results.append(&mut fields.clone());
127 }
128 }
129 results
130 }
131
132 pub fn add_subconf(&mut self, conf: CLNConf) -> Result<(), ParsingError> {
133 for subconf in &self.includes {
134 if conf.path == subconf.path {
135 return Err(ParsingError {
136 core: 2,
137 cause: format!("duplicate include {}", conf.path),
138 });
139 }
140 }
141 self.includes.push(conf.into());
142 Ok(())
143 }
144
145 pub fn rm_conf(&mut self, key: &str, val: Option<&str>) -> Result<(), ParsingError> {
146 if self.fields.contains_key(key) {
147 match val {
148 Some(val) => {
149 let values = self.fields.get_mut(key).unwrap();
150 if let Some(index) = values.iter().position(|x| x == val) {
151 values.remove(index);
152 } else {
153 return Err(ParsingError {
154 core: 2,
155 cause: format!("field {key} with value {val} not found"),
156 });
157 }
158 }
159 None => {
160 self.fields.remove_entry(key);
161 }
162 }
163 } else {
164 return Err(ParsingError {
165 core: 2,
166 cause: format!("field with `{key}` not present"),
167 });
168 }
169 Ok(())
170 }
171
172 pub fn flush(&self) -> Result<(), std::io::Error> {
173 let content = format!("{self}");
174 let file = File::new(&self.path);
175 file.write(&content)?;
176 Ok(())
177 }
178}
179
180impl SyncCLNConf for CLNConf {
181 fn parse(&mut self) -> Result<(), ParsingError> {
182 let parser = self.parser();
183 parser.parse(self)?;
184 Ok(())
185 }
186}
187
188impl fmt::Display for CLNConf {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 let mut content = String::new();
191 for field in self.fields.keys() {
192 let values = self.fields.get(field).unwrap();
193 if field.starts_with("comment") {
194 let value = values.first().unwrap().as_str();
195 content += &format!("{value}\n");
196 continue;
197 }
198 for value in values {
199 if value.is_empty() {
200 content += format!("{field}\n").as_str();
201 continue;
202 }
203 content += format!("{field}={value}\n").as_str();
204 }
205 }
206
207 for include in &self.includes {
208 content += format!("include {}\n", include.path).as_str();
209 }
210
211 writeln!(f, "{content}")
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::env;
218 use std::fs::{remove_file, File};
219 use std::io::Write;
220 use std::time::{SystemTime, UNIX_EPOCH};
221
222 use crate::{CLNConf, SyncCLNConf};
223
224 fn get_conf_path() -> String {
225 let binding = env::temp_dir();
226 let dir = binding.as_os_str().to_str().unwrap();
227 let nanos = SystemTime::now()
228 .duration_since(UNIX_EPOCH)
229 .unwrap()
230 .subsec_nanos();
231 format!("{dir}/conf-{nanos}")
232 }
233
234 fn build_file(content: &str) -> Result<String, std::io::Error> {
235 let conf = get_conf_path();
236 let mut file = File::create(conf.clone())?;
237 write!(file, "{content}")?;
238 Ok(conf)
239 }
240
241 fn cleanup_file(path: &str) {
242 remove_file(path).unwrap();
243 }
244
245 #[test]
246 fn parsing_key_value_one() {
247 let path = build_file("plugin=foo\nnetwork=bitcoin");
248 assert!(path.is_ok());
249 let path = path.unwrap();
250 let mut conf = CLNConf::new(path.to_string(), false);
251 let result = conf.parse();
252 assert!(result.is_ok());
253 assert_eq!(conf.fields.keys().len(), 2);
254
255 assert!(conf.fields.contains_key("plugin"));
256 assert!(conf.fields.contains_key("network"));
257
258 cleanup_file(path.as_str());
259 }
260
261 #[test]
262 fn flush_conf_one() {
263 let path = get_conf_path();
264 let mut conf = CLNConf::new(path.to_string(), false);
265 conf.add_conf("plugin", "/some/path");
266 conf.add_conf("network", "bitcoin");
267 let result = conf.flush();
268 assert!(result.is_ok());
269
270 let mut conf = CLNConf::new(path.to_string(), false);
271 let result = conf.parse();
272 assert!(result.is_ok());
273 assert_eq!(conf.fields.keys().len(), 2);
274 println!("{conf:?}");
275 assert!(conf.fields.contains_key("plugin"));
276 assert!(conf.fields.contains_key("network"));
277
278 cleanup_file(path.as_str());
279 }
280
281 #[test]
282 fn flush_conf_two() {
283 let path = get_conf_path();
284 let mut conf = CLNConf::new(path.to_string(), false);
285 conf.add_conf("plugin", "/some/path");
286 conf.add_conf("plugin", "foo");
287 conf.add_conf("network", "bitcoin");
288 let result = conf.flush();
289 assert!(result.is_ok());
290
291 let mut conf = CLNConf::new(path.to_string(), false);
292 let result = conf.parse();
293 assert!(result.is_ok());
294 assert_eq!(conf.fields.get("plugin").unwrap().len(), 2);
295 println!("{conf:?}");
296 assert!(conf.fields.contains_key("plugin"));
297 assert!(conf.fields.contains_key("network"));
298
299 cleanup_file(path.as_str());
300 }
301
302 #[test]
303 fn flush_conf_three() {
304 let path = get_conf_path();
305 let mut conf = CLNConf::new(path.to_string(), false);
306 conf.add_conf("network", "bitcoin");
307 conf.add_conf("plugin", "/some/path");
308 conf.add_conf("plugin", "/some/other/path");
309 conf.rm_conf("plugin", None);
310 let result = conf.flush();
311 assert!(result.is_ok());
312
313 let mut conf = CLNConf::new(path.to_string(), false);
314 let result = conf.parse();
315 assert!(result.is_ok());
316 assert_eq!(conf.fields.keys().len(), 1);
317 println!("{conf:?}");
318 assert!(!conf.fields.contains_key("plugin"));
319 assert!(conf.fields.contains_key("network"));
320
321 cleanup_file(path.as_str());
322 }
323
324 #[test]
325 fn flush_conf_four() {
326 let path = get_conf_path();
327 let mut conf = CLNConf::new(path.to_string(), false);
328 conf.add_conf("network", "bitcoin");
329 conf.add_conf("plugin", "/some/path");
330 conf.add_conf("plugin", "/some/other/path");
331 conf.rm_conf("plugin", Some("/some/other/path"));
332 let result = conf.flush();
333 assert!(result.is_ok());
334
335 let mut conf = CLNConf::new(path.to_string(), false);
336 let result = conf.parse();
337 assert!(result.is_ok());
338 assert_eq!(conf.fields.keys().len(), 2);
339 println!("{conf:?}");
340 assert!(conf
341 .fields
342 .get("plugin")
343 .as_ref()
344 .map(|&s| s.contains(&"/some/path".to_string()))
345 .unwrap_or(false));
346 assert!(!conf
347 .fields
348 .get("plugin")
349 .as_ref()
350 .map(|&s| s.contains(&"/some/other/path".to_string()))
351 .unwrap_or(false));
352 assert!(conf.fields.contains_key("network"));
353
354 cleanup_file(path.as_str());
355 }
356
357 #[test]
358 fn flush_conf_with_comments() {
359 let path = build_file("# this is just a commit\nplugin=foo\nnetwork=bitcoin");
360 assert!(path.is_ok());
361 let path = path.unwrap();
362 let mut conf = CLNConf::new(path.to_string(), false);
363 let result = conf.parse();
364 assert!(result.is_ok());
365 assert_eq!(conf.fields.keys().len() - 1, 2);
367
368 assert!(conf.fields.contains_key("plugin"));
369 assert!(conf.fields.contains_key("network"));
370
371 cleanup_file(path.as_str());
372 }
373
374 #[test]
375 fn flush_conf_with_includes() {
376 let subpath = get_conf_path();
377 let conf = CLNConf::new(subpath.clone(), false);
378 assert!(conf.flush().is_ok());
379
380 let path = build_file(
381 format!("# this is just a commit\nplugin=foo\nnetwork=bitcoin\ninclude {subpath}")
382 .as_str(),
383 );
384 assert!(path.is_ok(), "{}", format!("{path:?}"));
385 let path = path.unwrap();
386 let mut conf = CLNConf::new(path.to_string(), false);
387 let result = conf.parse();
388 assert!(result.is_ok(), "{}", result.unwrap_err().cause);
389 assert_eq!(conf.fields.keys().len() - 1, 2);
391
392 assert!(conf.fields.contains_key("plugin"));
393 assert!(conf.fields.contains_key("network"));
394
395 cleanup_file(path.as_str());
396 }
397}