Skip to main content

sim_config/
table.rs

1//! Config Table and Dir data types.
2
3use sim_kernel::{Expr, Symbol};
4
5use crate::{ConfigError, ConfigResult};
6
7/// One library's configuration table.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct ConfigTable {
10    /// Library id whose configuration this table carries.
11    pub lib: Symbol,
12    /// Configuration data; always an `Expr::Map` when built by this crate.
13    pub table: Expr,
14}
15
16impl ConfigTable {
17    /// Creates a table, rejecting non-map expressions.
18    pub fn new(lib: Symbol, table: Expr) -> ConfigResult<Self> {
19        if !matches!(table, Expr::Map(_)) {
20            return Err(ConfigError::NonTableExpr);
21        }
22        Ok(Self { lib, table })
23    }
24
25    /// Borrows this table's map entries.
26    pub fn entries(&self) -> ConfigResult<&[(Expr, Expr)]> {
27        match &self.table {
28            Expr::Map(entries) => Ok(entries),
29            _ => Err(ConfigError::NonTableExpr),
30        }
31    }
32}
33
34/// A configuration Dir: library id to table.
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct ConfigDir {
37    /// Tables carried by this Dir.
38    pub entries: Vec<ConfigTable>,
39}
40
41impl ConfigDir {
42    /// Creates an empty Dir.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Creates a Dir with one library table.
48    pub fn one(lib: Symbol, table: Expr) -> ConfigResult<Self> {
49        Ok(Self {
50            entries: vec![ConfigTable::new(lib, table)?],
51        })
52    }
53
54    /// Finds a table by library id.
55    pub fn table(&self, lib: &Symbol) -> Option<&ConfigTable> {
56        self.entries.iter().find(|entry| &entry.lib == lib)
57    }
58
59    /// Finds a mutable table by library id.
60    pub fn table_mut(&mut self, lib: &Symbol) -> Option<&mut ConfigTable> {
61        self.entries.iter_mut().find(|entry| &entry.lib == lib)
62    }
63
64    /// Inserts or replaces a table for its library id.
65    pub fn upsert(&mut self, table: ConfigTable) {
66        if let Some(existing) = self.table_mut(&table.lib) {
67            *existing = table;
68        } else {
69            self.entries.push(table);
70        }
71    }
72
73    /// Builds a Dir from a map of library id keys to table values.
74    pub fn from_dir_expr(expr: &Expr) -> ConfigResult<Self> {
75        let Expr::Map(pairs) = expr else {
76            return Err(ConfigError::NonDirExpr);
77        };
78        let mut entries = Vec::new();
79        for (key, value) in pairs {
80            let lib = symbol_key(key)?;
81            if !matches!(value, Expr::Map(_)) {
82                return Err(ConfigError::NonTableConfig { lib });
83            }
84            entries.push(ConfigTable {
85                lib,
86                table: value.clone(),
87            });
88        }
89        Ok(Self { entries })
90    }
91
92    /// Converts this Dir back to a kernel map expression.
93    pub fn to_expr(&self) -> Expr {
94        Expr::Map(
95            self.entries
96                .iter()
97                .map(|entry| (Expr::Symbol(entry.lib.clone()), entry.table.clone()))
98                .collect(),
99        )
100    }
101}
102
103/// Parses a library id string into a [`Symbol`].
104pub fn lib_symbol_from_str(id: &str) -> ConfigResult<Symbol> {
105    if id.is_empty() {
106        return Err(ConfigError::InvalidLibId { id: id.to_owned() });
107    }
108    match id.split_once('/') {
109        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
110            if name.contains('/') {
111                return Err(ConfigError::InvalidLibId { id: id.to_owned() });
112            }
113            Ok(Symbol::qualified(
114                Symbol::checked(namespace)?.name,
115                Symbol::checked(name)?.name,
116            ))
117        }
118        Some(_) => Err(ConfigError::InvalidLibId { id: id.to_owned() }),
119        None => Ok(Symbol::checked(id)?),
120    }
121}
122
123fn symbol_key(key: &Expr) -> ConfigResult<Symbol> {
124    match key {
125        Expr::Symbol(symbol) => Ok(symbol.clone()),
126        Expr::String(text) => lib_symbol_from_str(text),
127        other => Err(ConfigError::UnsupportedDirKey { key: other.clone() }),
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use sim_value::build::{map, text};
134
135    use super::*;
136
137    #[test]
138    fn dir_expr_accepts_symbol_and_string_keys() {
139        let expr = Expr::Map(vec![
140            (
141                Expr::Symbol(Symbol::qualified("sim", "cookbook")),
142                map(vec![("minimum_loaded", Expr::List(vec![]))]),
143            ),
144            (
145                Expr::String("stream/host".to_owned()),
146                map(vec![("audio_backend_regex", text("modeled"))]),
147            ),
148        ]);
149
150        let dir = ConfigDir::from_dir_expr(&expr).unwrap();
151
152        assert!(dir.table(&Symbol::qualified("sim", "cookbook")).is_some());
153        assert!(dir.table(&Symbol::qualified("stream", "host")).is_some());
154    }
155
156    #[test]
157    fn dir_expr_rejects_non_table_entries() {
158        let expr = Expr::Map(vec![(Expr::String("sim/cookbook".to_owned()), Expr::Nil)]);
159
160        let error = ConfigDir::from_dir_expr(&expr).unwrap_err();
161
162        assert_eq!(
163            error,
164            ConfigError::NonTableConfig {
165                lib: Symbol::qualified("sim", "cookbook")
166            }
167        );
168    }
169}