1use sim_kernel::{Expr, Symbol};
4
5use crate::{ConfigError, ConfigResult};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct ConfigTable {
10 pub lib: Symbol,
12 pub table: Expr,
14}
15
16impl ConfigTable {
17 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 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct ConfigDir {
37 pub entries: Vec<ConfigTable>,
39}
40
41impl ConfigDir {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn one(lib: Symbol, table: Expr) -> ConfigResult<Self> {
49 Ok(Self {
50 entries: vec![ConfigTable::new(lib, table)?],
51 })
52 }
53
54 pub fn table(&self, lib: &Symbol) -> Option<&ConfigTable> {
56 self.entries.iter().find(|entry| &entry.lib == lib)
57 }
58
59 pub fn table_mut(&mut self, lib: &Symbol) -> Option<&mut ConfigTable> {
61 self.entries.iter_mut().find(|entry| &entry.lib == lib)
62 }
63
64 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 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 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
103pub 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}