axconfig_gen/
config.rs

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