Skip to main content

ax_config_gen/
config.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use toml_edit::{Decor, DocumentMut, Item, Table, Value};
4
5use crate::{
6    ConfigErr, ConfigResult, ConfigType, ConfigValue,
7    output::{Output, OutputFormat},
8};
9
10type ConfigTable = BTreeMap<String, ConfigItem>;
11
12/// A structure representing a config item.
13///
14/// It contains the config key, value and comments.
15#[derive(Debug, Clone)]
16pub struct ConfigItem {
17    table_name: String,
18    key: String,
19    value: ConfigValue,
20    comments: String,
21}
22
23impl ConfigItem {
24    fn new(table_name: &str, table: &Table, key: &str, value: &Value) -> ConfigResult<Self> {
25        let inner = || {
26            let item = table.key(key).unwrap();
27            let comments = prefix_comments(item.leaf_decor())
28                .unwrap_or_default()
29                .to_string();
30            let suffix = suffix_comments(value.decor()).unwrap_or_default().trim();
31            let value = if !suffix.is_empty() {
32                let ty_str = suffix.trim_start_matches('#');
33                let ty = ConfigType::new(ty_str)?;
34                ConfigValue::from_raw_value_type(value, ty)?
35            } else {
36                ConfigValue::from_raw_value(value)?
37            };
38            Ok(Self {
39                table_name: table_name.into(),
40                key: key.into(),
41                value,
42                comments,
43            })
44        };
45        let res = inner();
46        if let Err(e) = &res {
47            eprintln!("Parsing error at key `{}`: {:?}", key, e);
48        }
49        res
50    }
51
52    fn new_global(table: &Table, key: &str, value: &Value) -> ConfigResult<Self> {
53        Self::new(Config::GLOBAL_TABLE_NAME, table, key, value)
54    }
55
56    /// Returns the unique name of the config item.
57    ///
58    /// If the item is contained in the global table, it returns the iten key.
59    /// Otherwise, it returns a string with the format `table.key`.
60    pub fn item_name(&self) -> String {
61        if self.table_name == Config::GLOBAL_TABLE_NAME {
62            self.key.clone()
63        } else {
64            format!("{}.{}", self.table_name, self.key)
65        }
66    }
67
68    /// Returns the table name of the config item.
69    pub fn table_name(&self) -> &str {
70        &self.table_name
71    }
72
73    /// Returns the key of the config item.
74    pub fn key(&self) -> &str {
75        &self.key
76    }
77
78    /// Returns the value of the config item.
79    pub fn value(&self) -> &ConfigValue {
80        &self.value
81    }
82
83    /// Returns the comments of the config item.
84    pub fn comments(&self) -> &str {
85        &self.comments
86    }
87
88    /// Returns the mutable reference to the value of the config item.
89    pub fn value_mut(&mut self) -> &mut ConfigValue {
90        &mut self.value
91    }
92}
93
94/// A structure storing all config items.
95///
96/// It contains a global table and multiple named tables, each table is a map
97/// from key to value, the key is a string and the value is a [`ConfigItem`].
98#[derive(Default, Debug)]
99pub struct Config {
100    global: ConfigTable,
101    tables: BTreeMap<String, ConfigTable>,
102    table_comments: BTreeMap<String, String>,
103}
104
105impl Config {
106    /// The name of the global table of the config.
107    pub const GLOBAL_TABLE_NAME: &'static str = "$GLOBAL";
108
109    /// Create a new empty config object.
110    pub fn new() -> Self {
111        Self {
112            global: ConfigTable::new(),
113            tables: BTreeMap::new(),
114            table_comments: BTreeMap::new(),
115        }
116    }
117
118    /// Returns whether the config object contains no items.
119    pub fn is_empty(&self) -> bool {
120        self.global.is_empty() && self.tables.is_empty()
121    }
122
123    fn new_table(&mut self, name: &str, comments: &str) -> ConfigResult<&mut ConfigTable> {
124        if name == Self::GLOBAL_TABLE_NAME {
125            return Err(ConfigErr::Other(format!(
126                "Table name `{}` is reserved",
127                Self::GLOBAL_TABLE_NAME
128            )));
129        }
130        if self.tables.contains_key(name) {
131            return Err(ConfigErr::Other(format!("Duplicate table name `{}`", name)));
132        }
133        self.tables.insert(name.into(), ConfigTable::new());
134        self.table_comments.insert(name.into(), comments.into());
135        Ok(self.tables.get_mut(name).unwrap())
136    }
137
138    /// Returns the global table of the config.
139    pub fn global_table(&self) -> &BTreeMap<String, ConfigItem> {
140        &self.global
141    }
142
143    /// Returns the reference to the table with the specified name.
144    pub fn table_at(&self, name: &str) -> Option<&BTreeMap<String, ConfigItem>> {
145        if name == Self::GLOBAL_TABLE_NAME {
146            Some(&self.global)
147        } else {
148            self.tables.get(name)
149        }
150    }
151
152    /// Returns the mutable reference to the table with the specified name.
153    pub fn table_at_mut(&mut self, name: &str) -> Option<&mut BTreeMap<String, ConfigItem>> {
154        if name == Self::GLOBAL_TABLE_NAME {
155            Some(&mut self.global)
156        } else {
157            self.tables.get_mut(name)
158        }
159    }
160
161    /// Returns the reference to the config item with the specified table name and key.
162    pub fn config_at(&self, table: &str, key: &str) -> Option<&ConfigItem> {
163        self.table_at(table).and_then(|t| t.get(key))
164    }
165
166    /// Returns the mutable reference to the config item with the specified
167    /// table name and key.
168    pub fn config_at_mut(&mut self, table: &str, key: &str) -> Option<&mut ConfigItem> {
169        self.table_at_mut(table).and_then(|t| t.get_mut(key))
170    }
171
172    /// Returns the comments of the table with the specified name.
173    pub fn table_comments_at(&self, name: &str) -> Option<&str> {
174        self.table_comments.get(name).map(|s| s.as_str())
175    }
176
177    /// Returns the iterator of all tables.
178    ///
179    /// The iterator returns a tuple of table name, table and comments. The
180    /// global table is named `$GLOBAL`.
181    pub fn table_iter(&self) -> impl Iterator<Item = (&str, &ConfigTable, &str)> {
182        let global_iter = [(Self::GLOBAL_TABLE_NAME, &self.global, "")].into_iter();
183        let other_iter = self.tables.iter().map(|(name, configs)| {
184            (
185                name.as_str(),
186                configs,
187                self.table_comments.get(name).unwrap().as_str(),
188            )
189        });
190        global_iter.chain(other_iter)
191    }
192
193    /// Returns the iterator of all config items.
194    ///
195    /// The iterator returns a tuple of table name, key and config item. The
196    /// global table is named `$GLOBAL`.
197    pub fn iter(&self) -> impl Iterator<Item = &ConfigItem> {
198        self.table_iter().flat_map(|(_, c, _)| c.values())
199    }
200}
201
202impl Config {
203    /// Parse a toml string into a config object.
204    pub fn from_toml(toml: &str) -> ConfigResult<Self> {
205        let doc = toml.parse::<DocumentMut>()?;
206        let table = doc.as_table();
207
208        let mut result = Self::new();
209        for (key, item) in table.iter() {
210            match item {
211                Item::Value(val) => {
212                    result
213                        .global
214                        .insert(key.into(), ConfigItem::new_global(table, key, val)?);
215                }
216                Item::Table(table) => {
217                    let table_name = key;
218                    let comments = prefix_comments(table.decor());
219                    let configs = result.new_table(key, comments.unwrap_or_default())?;
220                    for (key, item) in table.iter() {
221                        if let Item::Value(val) = item {
222                            configs
223                                .insert(key.into(), ConfigItem::new(table_name, table, key, val)?);
224                        } else {
225                            return Err(ConfigErr::InvalidValue);
226                        }
227                    }
228                }
229                Item::None => {}
230                _ => {
231                    return Err(ConfigErr::Other(format!(
232                        "Object array `[[{}]]` is not supported",
233                        key
234                    )));
235                }
236            }
237        }
238        Ok(result)
239    }
240
241    /// Dump the config into a string with the specified format.
242    pub fn dump(&self, fmt: OutputFormat) -> ConfigResult<String> {
243        let mut output = Output::new(fmt);
244        for (name, table, comments) in self.table_iter() {
245            if name != Self::GLOBAL_TABLE_NAME {
246                output.table_begin(name, comments);
247            }
248            for (key, item) in table.iter() {
249                if let Err(e) = output.write_item(item) {
250                    eprintln!("Dump config `{}` failed: {:?}", key, e);
251                }
252            }
253            if name != Self::GLOBAL_TABLE_NAME {
254                output.table_end();
255            }
256        }
257        Ok(output.result().into())
258    }
259
260    /// Dump the config into TOML format.
261    pub fn dump_toml(&self) -> ConfigResult<String> {
262        self.dump(OutputFormat::Toml)
263    }
264
265    /// Dump the config into Rust code.
266    pub fn dump_rs(&self) -> ConfigResult<String> {
267        self.dump(OutputFormat::Rust)
268    }
269
270    /// Merge the other config into `self`, if there is a duplicate key, return an error.
271    pub fn merge(&mut self, other: &Self) -> ConfigResult<()> {
272        for (name, other_table, table_comments) in other.table_iter() {
273            let self_table = if let Some(table) = self.table_at_mut(name) {
274                table
275            } else {
276                self.new_table(name, table_comments)?
277            };
278            for (key, item) in other_table.iter() {
279                if self_table.contains_key(key) {
280                    return Err(ConfigErr::Other(format!("Duplicate key `{}`", key)));
281                } else {
282                    self_table.insert(key.into(), item.clone());
283                }
284            }
285        }
286        Ok(())
287    }
288
289    /// Update the values of `self` with the other config, if there is a key not
290    /// found in `self`, skip it.
291    ///
292    /// It returns two vectors of `ConfigItem`, the first contains the keys that
293    /// are included in `self` but not in `other`, the second contains the keys
294    /// that are included in `other` but not in `self`.
295    pub fn update(&mut self, other: &Self) -> ConfigResult<(Vec<ConfigItem>, Vec<ConfigItem>)> {
296        let mut touched = BTreeSet::new(); // included in both `self` and `other`
297        let mut extra = Vec::new(); // included in `other` but not in `self`
298
299        for other_item in other.iter() {
300            let table_name = other_item.table_name.clone();
301            let key = other_item.key.clone();
302            let self_table = if let Some(table) = self.table_at_mut(&table_name) {
303                table
304            } else {
305                extra.push(other_item.clone());
306                continue;
307            };
308
309            if let Some(self_item) = self_table.get_mut(&key) {
310                self_item.value.update(other_item.value.clone())?;
311                touched.insert(self_item.item_name());
312            } else {
313                extra.push(other_item.clone());
314            }
315        }
316
317        // included in `self` but not in `other`
318        let untouched = self
319            .iter()
320            .filter(|item| !touched.contains(&item.item_name()))
321            .cloned()
322            .collect::<Vec<_>>();
323        Ok((untouched, extra))
324    }
325}
326
327fn prefix_comments(decor: &Decor) -> Option<&str> {
328    decor.prefix().and_then(|s| s.as_str())
329}
330
331fn suffix_comments(decor: &Decor) -> Option<&str> {
332    decor.suffix().and_then(|s| s.as_str())
333}