config_tools/
config.rs

1use crate::{builder::ConfigBuilder, error::Error, outcome::LoadOutcome};
2use ini::Ini;
3use std::{collections::BTreeMap, path::Path};
4
5pub trait Section: Sized {
6    fn from_section(map: &BTreeMap<String, String>) -> Result<Self, Error>;
7}
8
9/// Represents an INI-style configuration, including both general
10/// values (not tied to any section) and sectioned key-value pairs.
11///
12/// You can build a `Config` manually using the [`ConfigBuilder`] API,
13/// load one from a file, or create defaults using macros like
14/// [`crate::sectioned_defaults!`] and [`crate::general_defaults!`].
15#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
16pub struct Config {
17    pub sections: BTreeMap<String, BTreeMap<String, String>>,
18    pub general_values: BTreeMap<String, String>,
19}
20
21impl Config {
22    pub fn general(&self) -> &BTreeMap<String, String> {
23        &self.general_values
24    }
25
26    pub fn get(&self, section: Option<&str>, key: &str) -> Option<String> {
27        if let Some(section) = section {
28            return self.sections.get(section).and_then(|s| s.get(key)).cloned();
29        } else {
30            return self.general_values.get(key).cloned();
31        }
32    }
33
34    pub fn get_as<T>(&self, section: Option<&str>, key: &str) -> Option<T>
35    where
36        T: std::str::FromStr + std::fmt::Debug,
37    {
38        self.get(section, key).and_then(|v| v.parse().ok())
39    }
40
41    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
42        let ini = Ini::load_from_file(path).map_err(Error::ConfigLoad)?;
43        let mut sections = BTreeMap::new();
44        let mut general_values = BTreeMap::new();
45
46        for (section, prop) in ini.iter() {
47            if let Some(section) = section {
48                let mut section_map = BTreeMap::new();
49                prop.iter().for_each(|(key, value)| {
50                    section_map.insert(key.to_string(), value.to_string());
51                });
52
53                sections.insert(section.to_string(), section_map);
54            } else {
55                prop.iter().for_each(|(key, value)| {
56                    general_values.insert(key.to_string(), value.to_string());
57                })
58            }
59        }
60
61        Ok(Config {
62            sections,
63            general_values,
64        })
65    }
66
67    pub fn load_or_default<P: AsRef<Path>>(path: P, default: Config) -> Self {
68        match Self::load(path) {
69            Ok(config) => config,
70            Err(_) => default,
71        }
72    }
73
74    pub fn load_or_default_outcome<P: AsRef<Path>>(path: P, default: Config) -> LoadOutcome {
75        match Self::load(path) {
76            Ok(config) => LoadOutcome::FromFile(config),
77            Err(_) => LoadOutcome::FromDefault(default),
78        }
79    }
80
81    pub fn builder() -> ConfigBuilder {
82        ConfigBuilder {
83            config: Config::default(),
84            section: None,
85        }
86    }
87
88    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<&Self, Error> {
89        let mut ini = Ini::new();
90
91        let mut section = ini.with_general_section();
92        for (key, value) in &self.general_values {
93            section.set(key, value);
94        }
95
96        for (title, prop) in &self.sections {
97            let mut section = ini.with_section(Some(title));
98            for (key, value) in prop {
99                section.set(key, value);
100            }
101        }
102
103        ini.write_to_file(path).map_err(Error::ConfigCreation)?;
104
105        Ok(self)
106    }
107
108    pub fn section(&self, title: &str) -> Option<&BTreeMap<String, String>> {
109        self.sections.get(title)
110    }
111
112    pub fn sections(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
113        &self.sections
114    }
115
116    pub fn update(&mut self, section: Option<&str>, key: &str, value: &str) -> &mut Self {
117        if let Some(section) = section {
118            self.sections
119                .entry(section.to_string())
120                .or_default()
121                .insert(key.to_string(), value.to_string());
122        } else {
123            self.general_values
124                .insert(key.to_string(), value.to_string());
125        }
126
127        self
128    }
129}